<?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: Mihai Perdum</title>
    <description>The latest articles on DEV Community by Mihai Perdum (@mihai_leanzero).</description>
    <link>https://dev.to/mihai_leanzero</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%2F4130453%2Fdb62e2a0-c593-4f6c-a75e-49ca13efbeb5.jpeg</url>
      <title>DEV Community: Mihai Perdum</title>
      <link>https://dev.to/mihai_leanzero</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mihai_leanzero"/>
    <language>en</language>
    <item>
      <title>Atlassian Teamwork Graph API: Forge Rovo Agent</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Mon, 02 Nov 2026 09:00:15 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/atlassian-teamwork-graph-api-forge-rovo-agent-3mcc</link>
      <guid>https://dev.to/mihai_leanzero/atlassian-teamwork-graph-api-forge-rovo-agent-3mcc</guid>
      <description>&lt;p&gt;The Teamwork Graph API lets a Forge app ask Atlassian who works on what. I wired it into an agent for Rovo, Atlassian's AI assistant, that you can chat with in Jira. Ask it "what is my team working on?" and it reads each member of your Atlassian team and their assigned work items from the graph, then Rovo writes the reply. On our test site it answered all four of my questions correctly, and its actions ran in 0.8 to 2.6 seconds each.&lt;/p&gt;

&lt;p&gt;The API went to Preview on 15 October 2026, so search snippets that still say Early Access Program (EAP) are out of date, and even some Atlassian page titles still say EAP. Apps now use "curated" queries, which are ready-made, typed GraphQL queries that Atlassian defines. They also use one permission scope per object type, like read:graph:issue:jira or read:graph:team:atlassian. The old broad scopes, read:graph:jira and read:graph:confluence, are being retired.&lt;/p&gt;

&lt;p&gt;I built this demo to learn the APIs. Codegeist 2026, Atlassian's hackathon, has a bonus prize for Teamwork Graph apps, so it's a good moment to learn them. The app is 234 lines of JavaScript, an 81-line manifest and an 11-line prompt, and every file is printed here in full because the repo isn't public.&lt;/p&gt;

&lt;p&gt;Most of my time went into small surprises, like an error that arrives as HTTP 200 and a rate limit that hands back an empty list. There are ten of them in the gotchas section.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt; — You need a Jira Cloud site on a Standard, Premium or Enterprise plan with Rovo turned on. Rovo is paid-only, so a free site may not have it, but a Standard or Premium trial includes it. You need organization admin rights on that site. You need Forge CLI: I used 14.1.0, and 15.0.0 is current and needs Node.js 22.23.4 or newer. You need &lt;a class="mentioned-user" href="https://dev.to/forge"&gt;@forge&lt;/a&gt;/api 8.4.0. Last, you need an Atlassian team with a few people who have Jira work items assigned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;[[steps]]&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a Jira Forge app&lt;/strong&gt; — run forge create, pick a Jira template, and keep the app id it writes into manifest.yml.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replace the manifest&lt;/strong&gt; — one rovo:agent, three GET actions and eight scopes (Step 1).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the prompt&lt;/strong&gt; — prompts/prompt.md tells the agent which action answers which question (Step 1).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the Teamwork Graph client&lt;/strong&gt; — src/twg.js names every operation, pages and retries (Step 2).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the actions&lt;/strong&gt; — src/index.js reads the person chatting and the work item on screen (Step 3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy and install&lt;/strong&gt; — an org admin installs it and approves the graph scopes (Step 4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask it four questions&lt;/strong&gt; — in Rovo chat on a Jira work item, then check the function logs (Step 5).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Teamwork Graph matters for Codegeist 2026
&lt;/h2&gt;

&lt;p&gt;Codegeist 2026 is on Devpost at &lt;a href="https://codegeist2026.devpost.com/" rel="noopener noreferrer"&gt;codegeist2026.devpost.com&lt;/a&gt;. The bare codegeist.devpost.com address still shows the 2024 edition, so check which one you're reading. Submissions opened 27 October 2026 at 9:00 am Pacific and close 22 December 2026 at 12:00 pm Pacific. Winners are due around 15 February 2027.&lt;/p&gt;

&lt;p&gt;Each of the three categories pays $15,000 for first place, $8,000 for second and $5,000 for third. On top of that, the &lt;a href="https://codegeist2026.devpost.com/rules" rel="noopener noreferrer"&gt;rules page&lt;/a&gt; has four bonus prizes worth $1,500 each, and each is awarded to six projects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bonus prize&lt;/th&gt;
&lt;th&gt;What qualifies (from the rules)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Best Agents - Rovo/Assignable agents&lt;/td&gt;
&lt;td&gt;an app using rovo:agent, rovo:agentConnector, or reusable actions other agents can call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best app with Atlassian MCP&lt;/td&gt;
&lt;td&gt;tools exposed through the rovo:mcp module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best apps with Teamwork Graph integration&lt;/td&gt;
&lt;td&gt;an app that "connects data into the graph and/or consumes data from the Teamwork Graph API"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best Marketplace published app&lt;/td&gt;
&lt;td&gt;published to the Marketplace or submitted for listing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"The same Submission can win up to one Grand Prize and one Bonus Prize." An app like this demo qualifies for two bonus prizes, Best Agents and Best apps with Teamwork Graph integration, but one entry can only win one of them. The Devpost pages don't say how entrants get a Rovo-enabled site. Atlassian's &lt;a href="https://www.atlassian.com/software/rovo/pricing" rel="noopener noreferrer"&gt;Rovo pricing page&lt;/a&gt; says you can "trial Rovo through a trial of our Standard, Premium, or Enterprise plan".&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Teamwork Graph API is, and its Preview status
&lt;/h2&gt;

&lt;p&gt;Teamwork Graph is Atlassian's connected model of what lives on a site and in an org: users, teams, Jira work items, comments, goals, Confluence pages, plus whatever third-party connectors add. You talk to it through GraphQL, at one endpoint per site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST https://&amp;lt;your-site&amp;gt;.atlassian.net/gateway/api/graphql/twg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Atlassian's &lt;a href="https://developer.atlassian.com/platform/teamwork-graph/curated-queries/" rel="noopener noreferrer"&gt;curated queries page&lt;/a&gt; (updated 25 September 2026) says it is "available through Forge's Preview release phase", which in practice means anyone can use it today with the same support as a generally available feature, and breaking changes get "a minimum 1-month deprecation notice" in the Teamwork Graph changelog before they ship. Each curated query is a typed root field, for example atlassianUserAssignedJiraWorkItems. You pass ids in and get typed nodes back. Paging uses first and after. Unlike persisted Cypher queries (Cypher is a graph query language; more on these in the gotchas), curated queries need no registration first.&lt;/p&gt;

&lt;p&gt;Two sentences on that page decided my design. The first says "Every row returned is permission-filtered for the authenticated user, so a curated query only ever returns what that user can already see." The second says "Curated queries cap first at 25 ... Requesting more than 25 returns an error." So my agent runs as the person chatting, and it always pages.&lt;/p&gt;

&lt;p&gt;The full schema downloads from &lt;code&gt;https://&amp;lt;your-site&amp;gt;.atlassian.net/gateway/api/graphql/twg/schema&lt;/code&gt; (basic auth with an API token works for this one URL). Mine was 172 KB. It's the quickest way I found to look up a field name. Jira's API still calls work items issues, so you'll see issueKey and read:graph:issue:jira next to "work item" everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Create a Rovo agent in a Jira Forge app: the manifest and Teamwork Graph scopes
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;forge create&lt;/code&gt;, pick a Jira template, and replace the generated manifest.yml with this one. Keep the &lt;code&gt;app.id&lt;/code&gt; line: forge create registered the app and wrote its id there. Delete any template src files you won't use. The manifest declares one rovo:agent, three action modules and their functions. All three actions are GET. I did that on purpose. The &lt;a href="https://developer.atlassian.com/platform/forge/manifest-reference/modules/rovo-action/" rel="noopener noreferrer"&gt;action docs&lt;/a&gt; say agents triggered by automation rules won't call CREATE, UPDATE, DELETE or TRIGGER actions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;modules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rovo:agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;work-context-guide&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Work Context Guide&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Answers who is working on what, and who is involved in a work item,&lt;/span&gt;
        &lt;span class="s"&gt;from Teamwork Graph.&lt;/span&gt;
      &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;resource:agent-prompt;prompt.md&lt;/span&gt;
      &lt;span class="na"&gt;conversationStarters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;What am I working on right now?&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Who is involved in this work item?&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;What is my team working on?&lt;/span&gt;
      &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;whos-working-on&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;work-item-people&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;team-workload&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;whos-working-on&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Who is working on what&lt;/span&gt;
      &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;whosWorkingOn&lt;/span&gt;
      &lt;span class="na"&gt;actionVerb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;Lists the Jira work items a person is assigned to, with status, and the Atlassian teams they belong to.&lt;/span&gt;
        &lt;span class="s"&gt;Use it for "what is X working on" or "what am I working on". Leave person empty for the user asking.&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;person&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Person&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;A person's name or email. Leave empty to mean the user who is asking.&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;work-item-people&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;People around a work item&lt;/span&gt;
      &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;workItemPeople&lt;/span&gt;
      &lt;span class="na"&gt;actionVerb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;For one Jira work item, lists who is assigned, who reported it, who commented recently, the assignee's&lt;/span&gt;
        &lt;span class="s"&gt;teams and any goals the item is linked to. Use it for "who is involved in X" or "who should I ask about X".&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;issueKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Work item key&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The Jira work item key, for example ORB-12. Leave empty to use the&lt;/span&gt;
            &lt;span class="s"&gt;work item open on screen.&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;team-workload&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Team workload&lt;/span&gt;
      &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;teamWorkload&lt;/span&gt;
      &lt;span class="na"&gt;actionVerb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;For an Atlassian team, lists its members and what each one is assigned. Use it for "what is my team&lt;/span&gt;
        &lt;span class="s"&gt;working on" or "what is the Platform team working on". Leave team empty for the asker's first team.&lt;/span&gt;
      &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;team&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Team name&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The team's name, or part of it. Leave empty for the team of the&lt;/span&gt;
            &lt;span class="s"&gt;user who is asking.&lt;/span&gt;
  &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;whosWorkingOn&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.whosWorkingOn&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;workItemPeople&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.workItemPeople&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;teamWorkload&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.teamWorkload&lt;/span&gt;
&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agent-prompt&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prompts&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;read:jira-user&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;read:jira-work&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;read:graph:issue:jira&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;read:graph:issue-status:jira&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;read:graph:issue-comment:jira&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;read:graph:user:atlassian&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;read:graph:team:atlassian&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;read:graph:goal:atlassian&lt;/span&gt;
&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nodejs22.x&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ari:cloud:ecosystem::app/&amp;lt;your-app-id&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://developer.atlassian.com/platform/forge/manifest-reference/modules/rovo-agent/" rel="noopener noreferrer"&gt;rovo:agent reference&lt;/a&gt; caps the name at 30 characters (ours is 18). Loading the prompt from a resource file needs Forge CLI 10.6.0 or later, and it keeps a long prompt out of the YAML. Rovo reads the action descriptions when it picks an action, so I wrote each one as the question it answers. Watch one mismatch, though. team-workload's description mentions "the Platform team", but the code only finds teams the asker belongs to (Step 3 says why).&lt;/p&gt;

&lt;p&gt;The scope names come from the &lt;a href="https://developer.atlassian.com/platform/teamwork-graph/scopes/" rel="noopener noreferrer"&gt;Teamwork Graph scopes page&lt;/a&gt; (15 September 2026), whose pattern is read:graph::. The curated queries page adds that a request needs "a scope for every object type the field returns, as well as for the object type it starts from". My queries touch six graph object types (user, team, work item, status, comment, goal), so six graph scopes. The two Jira scopes, read:jira-user and read:jira-work, are for the plain Jira REST calls the actions also make. The scopes page also says the broad read:graph:jira and read:graph:confluence scopes "are being retired", and that Forge caps an app at 50 scopes.&lt;/p&gt;


&lt;p&gt;Now create prompts/prompt.md. Here's my whole prompt:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight markdown"&gt;&lt;code&gt;You are Work Context Guide. You help people see who is working on what in their Atlassian site.

Use your actions, never guess:
&lt;span class="p"&gt;-&lt;/span&gt; "what is X working on" or "what am I working on": call whos-working-on (leave person empty for the asker).
&lt;span class="p"&gt;-&lt;/span&gt; "who is involved in ORB-12", "who should I ask about this": call work-item-people with the key, or leave it
  empty when the user is looking at a work item.
&lt;span class="p"&gt;-&lt;/span&gt; "what is my team working on" or a named team: call team-workload.

Answer in short plain sentences. Name people and work item keys exactly as the action returns them, and link
work items with the webUrl you get. If an action says a source could not be read (a "notes" entry), tell the
user that part may be incomplete instead of saying there is nothing. Never invent people, teams or items.
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Don't skip the prompt's last paragraph. An empty answer from the graph doesn't always mean there is nothing there, and that paragraph tells the agent to say the answer may be incomplete instead of saying there is nothing.&lt;/p&gt;

&lt;p&gt;Don't expect a clean &lt;code&gt;forge lint&lt;/code&gt; yet: the three handlers don't exist until Step 3, and lint should flag them as missing. You'll verify the whole app there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Call the Teamwork Graph API from Forge: curated queries, paging and retries
&lt;/h2&gt;

&lt;p&gt;Create src/twg.js with this, the whole file. twg() makes one call as the user and retries transient errors. twgAll() reads a paged list (GraphQL calls it a connection) 25 rows at a time. Q holds the five queries.&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Teamwork Graph (TWG) client for Forge backend functions.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Things that cost real time to learn (measured on a live site, Oct 2026):&lt;/span&gt;
&lt;span class="c1"&gt;// - Every operation needs a name shaped `[company]_[operation]` (company &amp;gt;= 3 chars, operation &amp;gt;= 4).&lt;/span&gt;
&lt;span class="c1"&gt;//   Without it the API answers HTTP 200 with a DataFetchingException, not a 4xx.&lt;/span&gt;
&lt;span class="c1"&gt;// - Raw cypherQuery is refused for apps. Use curated queries (typed root fields) or persisted queries.&lt;/span&gt;
&lt;span class="c1"&gt;// - Curated connections return at most 25 rows per page. Page with `after`.&lt;/span&gt;
&lt;span class="c1"&gt;// - A rate-limited call can come back with zero rows. Treat "empty plus errors" as "could not read",&lt;/span&gt;
&lt;span class="c1"&gt;//   never as "nothing there", and retry transient errors with backoff.&lt;/span&gt;
&lt;span class="c1"&gt;// - @forge/api exposes requestTeamworkGraph on asUser() only. asUser() answers with what the person&lt;/span&gt;
&lt;span class="c1"&gt;//   chatting can see. Calling the Teamwork Graph API as the app is undocumented and untested here,&lt;/span&gt;
&lt;span class="c1"&gt;//   so this demo uses asUser().&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@forge/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OP_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rovodemo_&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PAGE_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TRANSIENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/RATE_LIMIT|THROTTL|429|502|503|504/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sleep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userAri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`ari:cloud:identity::user/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workItemAri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cloudId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issueId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`ari:cloud:jira:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cloudId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:issue/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issueId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// One GraphQL call as the user, with up to 3 retries on transient errors.&lt;/span&gt;
&lt;span class="c1"&gt;// Answers { ok, data, error, ms } and never throws.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;twg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;operationName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OP_PREFIX&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;opName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;query OP&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`query &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;operationName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asUser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;requestTeamworkGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;operationName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;classification&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;extensions&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;errorType&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`http-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;firstError&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;TRANSIENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;firstError&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;firstError&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Reads a curated connection page by page. `field` is the root field name in the query.&lt;/span&gt;
&lt;span class="c1"&gt;// Answers { ok, nodes, complete, error }. complete=false means we stopped early (maxPages) or a page failed.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;twgAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxPages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxPages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;twg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PAGE_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-connection&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Keep paging while the API says there is more, even when a page came back short.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageInfo&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;hasNextPage&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageInfo&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;endCursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endCursor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stopped-after-max-pages&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userTeams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`query OP($userId: ID!, $first: Int, $after: String) {
    atlassianUserInTeams(userId: $userId, first: $first, after: $after) {
      edges { node { ... on AtlassianTeam { id displayName } } }
      pageInfo { hasNextPage endCursor }
    }
  }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;teamMembers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`query OP($teamAri: ID!, $first: Int, $after: String) {
    atlassianTeamMembers(teamAri: $teamAri, first: $first, after: $after) {
      edges { node { ... on AtlassianUser { accountId name } } }
      pageInfo { hasNextPage endCursor }
    }
  }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;assignedWork&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`query OP($userId: ID!, $first: Int, $after: String) {
    atlassianUserAssignedJiraWorkItems(userId: $userId, first: $first, after: $after) {
      edges { node { ... on JiraWorkItem { key summary webUrl status { name } } } }
      pageInfo { hasNextPage endCursor }
    }
  }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;workItemComments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`query OP($id: ID!, $since: String!, $first: Int, $after: String) {
    jiraWorkItemComments(id: $id, since: $since, first: $first, after: $after) {
      edges { node { ... on JiraWorkItemComment { createdAt author { name ... on AtlassianUser { accountId } } } } }
      pageInfo { hasNextPage endCursor }
    }
  }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;workItemGoals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`query OP($id: ID!, $first: Int, $after: String) {
    jiraWorkItemGoals(id: $id, first: $first, after: $after) {
      edges { node { ... on AtlassianGoal { name url } } }
      pageInfo { hasNextPage endCursor }
    }
  }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Change OP_PREFIX to your own company name first. The operation name rule (more in the gotchas) wants at least 3 characters before the underscore and at least 4 after it. So rovodemo_teamMembers passes, and rovodemo_get would not, because get has only 3 characters. Each query starts with &lt;code&gt;query OP&lt;/code&gt;, and twg() swaps in the real name.&lt;/p&gt;

&lt;p&gt;A call counts as good only when it has data and no errors, because the API reports some failures with HTTP 200. Transient errors get up to three retries, with backoff from 200 ms capped at 1.2 s plus jitter. twgAll keeps going while hasNextPage is true, stops after four pages by default, and says complete: false when it stopped early or a page failed.&lt;/p&gt;

&lt;p&gt;Ids in the graph are ARIs (Atlassian resource identifiers). For a user that's ari☁️identity::user/ plus the accountId. For a Jira work item it's ari☁️jira::issue/, with the numeric issue id, so the key won't do. Both helpers near the top of twg.js build them. You can't verify this file on its own yet; Step 5's logs confirm it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Write the Forge Rovo agent actions: accountId, issue key and ARIs
&lt;/h2&gt;

&lt;p&gt;Create src/index.js with this, again the whole file:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Work Context Guide: three Rovo actions that answer "who works on what" from Teamwork Graph.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@forge/api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;twg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;twgAll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userAri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;workItemAri&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./twg.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// Rovo puts the person chatting in payload.context.accountId.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;askerOf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Resolve a name or email to one Jira user, as the person asking (they only find who they can see).&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;findPerson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asUser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;requestJira&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="s2"&gt;`/rest/api/3/user/search?query=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;maxResults=5`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`user search failed (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;atlassian&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`no active person matches "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" matches &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; people: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;. Ask which one.`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;personName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asUser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;requestJira&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="s2"&gt;`/rest/api/3/user?accountId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workItemRow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;webUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webUrl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;teamsOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;twgAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;userTeams&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userTeams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;userAri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;atlassianUserInTeams&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`teams could not be read: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;team list may be incomplete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assignedTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxPages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;twgAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assignedWork&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assignedWork&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;userAri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;atlassianUserAssignedJiraWorkItems&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxPages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`assigned work could not be read: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`showing the first &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; assigned work items; there are more`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;workItemRow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Action 1: what is a person (or the asker) working on?&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;whosWorkingOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;asked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;person&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;person&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;findPerson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asked&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;askerOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I could not tell who is asking. Give me a name.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;person&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;personName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;workItems&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;teamsOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;assignedTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;workItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;done|closed|resolved&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;whos-working-on&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;workItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;person&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;person&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;assignedOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;assignedDoneCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;workItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Action 2: who is involved in one work item?&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;workItemPeople&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;issueKey&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;jira&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;issueKey&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Which work item? Give me a key such as ORB-12.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Jira REST as the user: proves they can see the item and gives us its numeric id for the ARI.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asUser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;requestJira&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="s2"&gt;`/rest/api/3/issue/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?fields=summary,assignee,reporter,status`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not exist or you cannot see it.`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Could not read &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;).`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cloudId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;cloudId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ari&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;workItemAri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cloudId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;twgAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;workItemComments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workItemComments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ari&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;since&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;P90D&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jiraWorkItemComments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;twgAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;workItemGoals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workItemGoals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ari&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jiraWorkItemGoals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`comments could not be read: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`goals could not be read: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Count comments per author.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;byAuthor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;byAuthor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lastAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastAt&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;byAuthor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assignee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assignee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assigneeTeams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;assignee&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;teamsOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;assignee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;work-item-people&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;workItem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;assignee&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;assignee&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;assignee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;assigneeTeams&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;commentersLast90Days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;byAuthor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;goals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Action 3: what is a team working on?&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;teamWorkload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;asker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;askerOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;asker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I could not tell who is asking.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wanted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Teams are found through the asker: the curated API starts from a user or a team id, not a team name.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myTeams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;teamsOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;asker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wanted&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;myTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wanted&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;myTeams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;wanted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You are not in a team called "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;". Your teams: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;myTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are not in any Atlassian team yet.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;twgAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;teamMembers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;teamMembers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;teamAri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;atlassianTeamMembers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Members of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; could not be read: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="c1"&gt;// One page of assigned work per member keeps the call well inside Rovo's ~20 s per action.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;work&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;assignedTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;done|closed|resolved&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;openCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`showing 12 of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; members`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;team-workload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;team&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;team&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;When Rovo calls an action, your function gets whatever inputs the model filled in plus a context object. payload.context.accountId is the person chatting. If they opened the chat on a work item, payload.context.jira has its issueKey and issueId, and payload.context.cloudId is the site.&lt;/p&gt;

&lt;p&gt;Why does workItemPeople read the work item through Jira REST first? A 404 tells the person plainly they can't see the item, and the response has the numeric id the ARI needs. You could take context.jira.issueId from the payload instead when the chat was opened on that item, if you don't care about the visibility check. The comments query passes since: "P90D", an ISO 8601 duration meaning the last 90 days.&lt;/p&gt;

&lt;p&gt;whosWorkingOn looks a name up through /rest/api/3/user/search, as the user, so people only find who they can see. If several people match, it asks which one. teamWorkload starts from the asker's own teams, because curated queries start from a user id or a team id and I found no lookup by team name. It reads the members, then one page of assigned work for each of up to 12 of them, to stay well under the time Rovo waits for an action (about 20 seconds in our tests). When a team member has more than 25 assigned items, teamWorkload adds a note that there are more.&lt;/p&gt;

&lt;p&gt;Each action returns JSON, and Rovo writes the prose. Anything that might be partial goes into notes, and the prompt turns that into "this may be incomplete". The check that hides finished work only matches the status names Done, Closed and Resolved, so a site that uses other status names or another language needs a different check. Also, personName falls back to the raw accountId if the lookup fails.&lt;/p&gt;

&lt;p&gt;Your folder should now hold manifest.yml, package.json, prompts/prompt.md, src/index.js and src/twg.js. Run &lt;code&gt;forge lint&lt;/code&gt; now to confirm the three handlers in the manifest match the three exports in src/index.js.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4. Deploy and install the Forge app (an org admin approves the graph scopes)
&lt;/h2&gt;

&lt;p&gt;Install the dependency, then deploy and install:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @forge/api@8.4.0
forge deploy &lt;span class="nt"&gt;-e&lt;/span&gt; development
forge &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; development &lt;span class="nt"&gt;--site&lt;/span&gt; &amp;lt;your-site&amp;gt;.atlassian.net &lt;span class="nt"&gt;--product&lt;/span&gt; jira
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;An org admin has to run the install, and they're asked to consent to the graph scopes. My deploy also said the version was "eligible for the Runs on Atlassian program", the badge Atlassian applies automatically to eligible Marketplace apps. In October 2026 the scopes were accepted with no allowlisting and no request to Atlassian. Two older pages, &lt;a href="https://developer.atlassian.com/platform/forge/call-the-teamwork-graph-api/" rel="noopener noreferrer"&gt;Call the Teamwork Graph API&lt;/a&gt; (February 2025) and its copy under the Teamwork Graph docs (February 2026), still list an "App not allowlisted" error and tell you to add read:graph:jira. Ignore that part and take scopes from the scopes page.&lt;/p&gt;

&lt;p&gt;To confirm it, run &lt;code&gt;forge install list&lt;/code&gt;. It lists every environment, so look for the development row with your site and Jira. If you change scopes later, that's a new major version. Atlassian's &lt;a href="https://developer.atlassian.com/platform/forge/versions/" rel="noopener noreferrer"&gt;app versions page&lt;/a&gt; says it "won't be applied to a site until its admin consents to the upgrade", so redeploy, then run:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;forge &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; development &lt;span class="nt"&gt;--site&lt;/span&gt; &amp;lt;your-site&amp;gt;.atlassian.net &lt;span class="nt"&gt;--product&lt;/span&gt; jira &lt;span class="nt"&gt;--upgrade&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Step 5. Try the Rovo agent in Jira: four questions and its answers
&lt;/h2&gt;

&lt;p&gt;Open a work item in Jira and click Open Rovo Chat. Click Change agent, choose Browse agents, and search for Work Context Guide there. I tried the small search box in the agent menu first. It didn't list my development agent. Browse agents did. You should see the agent's description and its three conversation starters:&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F6552b58945547ec22e76e8322109e36c5727a41d-798x1200.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F6552b58945547ec22e76e8322109e36c5727a41d-798x1200.png" title="The agent opened from ORB-3 in Rovo chat. Notice the three conversation starters from the manifest, and ORB-3 already in context: that's what fills context.jira.issueKey." alt="The Rovo chat panel with the Work Context Guide agent selected, before any question: its description says it answers who is working on what from Teamwork Graph, with three conversation starters underneath and ORB-3 shown as context" width="798" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I asked four questions as Theo, a test user who had never opened the app. Rovo actions didn't need him to grant the app anything first. All of this is from our test site on 30 October 2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What the agent answered&lt;/th&gt;
&lt;th&gt;Right?&lt;/th&gt;
&lt;th&gt;Action time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who is involved in this work item? (on ORB-3)&lt;/td&gt;
&lt;td&gt;assignee Theo (Orbit Mobile Squad), the reporter, commenters Iris and Elzi&lt;/td&gt;
&lt;td&gt;yes, though it left out Theo's own comment&lt;/td&gt;
&lt;td&gt;1.8 s, then 1.0 s on a repeat; 4 comments read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is the Orbit Mobile Squad team working on?&lt;/td&gt;
&lt;td&gt;Theo ORB-3 and ORB-6, Iris ORB-1, ORB-5 and ORB-7, Elzi ORB-2&lt;/td&gt;
&lt;td&gt;yes, and the Done item ORB-4 was left out&lt;/td&gt;
&lt;td&gt;2.6 s, 3 members&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is Iris Calder working on?&lt;/td&gt;
&lt;td&gt;ORB-1, ORB-7 and ORB-5, plus her team&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What am I working on right now?&lt;/td&gt;
&lt;td&gt;ORB-3 and ORB-6, plus the team&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;0.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F18cce1459baa402ff422c8e31ace050b1239e0db-2474x598.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F18cce1459baa402ff422c8e31ace050b1239e0db-2474x598.png" title="All four answers from the table, as Rovo showed them. Notice each work item key comes back as a live link with its status, because the actions return webUrl and status." alt="Four Rovo chat answers side by side: who is involved in ORB-3, what the Orbit Mobile Squad team is working on, what Iris Calder is working on, and what am I working on right now" width="798" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To check what the actions did behind the prose, read the function logs. Each action writes one JSON line:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;forge logs &lt;span class="nt"&gt;-e&lt;/span&gt; development
&lt;/code&gt;&lt;/pre&gt;





&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO    2026-10-30T20:39:26.105Z 8b86b8f4-cf67-4750-aa5e-7cbc6e0daa77 {"demo":"work-item-people","ms":1789,"key":"ORB-3","comments":4,"goals":0}
INFO    2026-10-30T20:41:05.809Z 39db3064-c401-4038-88cb-1700c7bb0a24 {"demo":"work-item-people","ms":1035,"key":"ORB-3","comments":4,"goals":0}
INFO    2026-10-30T20:41:36.988Z da043a4f-b66e-4806-af23-7670dfb84faf {"demo":"team-workload","ms":2556,"team":"Orbit Mobile Squad","members":3}
INFO    2026-10-30T20:42:51.366Z db186c8f-c779-417b-aae8-cfb05854311b {"demo":"whos-working-on","ms":1133,"teams":1,"items":3,"notes":0}
INFO    2026-10-30T20:43:24.939Z 3bc89011-0421-4ab4-9a2a-1a9e1cd01bdb {"demo":"whos-working-on","ms":822,"teams":1,"items":2,"notes":0}
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Each answer took 28 to 32 seconds to show up in the chat, while the action behind it ran in 0.8 to 2.6 seconds, so almost all of the wait was Rovo's model writing the reply. A 1-second action still gave me a 30-second chat.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F9a5e982e7f9afff65784b814244bcb8abab6e3a3-898x1400.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F9a5e982e7f9afff65784b814244bcb8abab6e3a3-898x1400.png" title="The team question in full. Notice ORB-4 is missing: it was Done, and the action only returns open work." alt="The Rovo chat answer to What is the Orbit Mobile Squad team working on: Theo Varnell on ORB-3 and ORB-6, Iris Calder on ORB-1, ORB-5 and ORB-7, and Elzi on ORB-2, each item with its status" width="800" height="1247"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the first answer Theo, who was asking, had also commented on ORB-3, and Rovo left him off the commenters even though the action returned him. I think the model probably decided he didn't need to hear his own name. I can't prove that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teamwork Graph API gotchas we measured
&lt;/h2&gt;

&lt;p&gt;I found these on our own sites in October 2026. It's a Preview API, so check each against the &lt;a href="https://developer.atlassian.com/platform/teamwork-graph/changelog/" rel="noopener noreferrer"&gt;Teamwork Graph changelog&lt;/a&gt; before you rely on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  You must set an operation name of the shape [company]_[operation]
&lt;/h3&gt;

&lt;p&gt;Send a query without a properly named operation and you get this, word for word:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You must set an operation name of the shape `[company]_[operation]` with company &amp;gt;= 3 chars and operation &amp;gt;= 4 chars and max length 100
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;It arrives with HTTP 200. Classification: DataFetchingException. Atlassian's docs only show the rule by example (their sample query is called companyName_assignedWork). Name every operation, and treat any errors array as a failure whatever the status code says.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F6a7795e83678981ef1c2eb1489b13a43b315db3b-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F6a7795e83678981ef1c2eb1489b13a43b315db3b-1600x896.png" title="Illustration, generated locally with ERNIE-Image. A badly named query comes back as HTTP 200 with the error inside the body. twg() reads the errors array for exactly this." alt="Illustration: a grey postman robot hands a blue robot a parcel stamped OK, and the blue robot pulls out a crumpled note that says 200 OK with a red cross on it" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Raw Cypher is refused: use a persisted query
&lt;/h3&gt;

&lt;p&gt;Some older examples send a cypherQuery string. That's gone. The &lt;a href="https://developer.atlassian.com/platform/teamwork-graph/migrate-from-raw-cypher/" rel="noopener noreferrer"&gt;migration page&lt;/a&gt; says "Apps can no longer send arbitrary Cypher text to Teamwork Graph at request time", and my app got an error telling it to use a persisted query instead. Curated queries covered everything this agent needed.&lt;/p&gt;

&lt;p&gt;For traversals they don't cover, you register &lt;a href="https://developer.atlassian.com/platform/teamwork-graph/persisted-cypher-queries/" rel="noopener noreferrer"&gt;persisted Cypher queries&lt;/a&gt; against your app and call them through customQuery. In our tests we could only register them from a logged-in browser session (a session cookie plus an Origin header); an API token didn't work. The persisted queries page says a new query takes about a minute to distribute.&lt;/p&gt;

&lt;h3&gt;
  
  
  requestTeamworkGraph with asUser vs asApp
&lt;/h3&gt;

&lt;p&gt;In &lt;a class="mentioned-user" href="https://dev.to/forge"&gt;@forge&lt;/a&gt;/api 8.4.0 you'll only find requestTeamworkGraph on asUser(). The AsAppFetchMethods type doesn't have it. asUser answers with what that person can see, and in a chat that's what you want.&lt;/p&gt;

&lt;p&gt;Can you call it as the app? It's not settled. A &lt;a href="https://community.developer.atlassian.com/t/teamwork-graph-calling-the-api-asapp/103402" rel="noopener noreferrer"&gt;developer community thread from 22 October&lt;/a&gt; asks exactly that and is still waiting for an official answer. The &lt;a href="https://developer.atlassian.com/platform/forge/apis-reference/ui-api-bridge/requestTeamworkGraph/" rel="noopener noreferrer"&gt;requestTeamworkGraph bridge page&lt;/a&gt; says calls from the frontend "always run as the current user - Forge apps can't call the Teamwork Graph API as the app", and points backend code to asUser(). If you do find a way to call it as the app, the results aren't filtered to what one person can see, so expect to do your own visibility checks before handing anything to an agent. I didn't test that part. Our &lt;a href="https://leanzero.net/tutorials/forge-app-access-rule-data-security-policy?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Forge app access rule tutorial&lt;/a&gt; covers a related case, where an admin policy hides content from the app.&lt;/p&gt;

&lt;h3&gt;
  
  
  RATE_LIMIT_EXCEEDED and empty results
&lt;/h3&gt;

&lt;p&gt;One caller in our tests hit RATE_LIMIT_EXCEEDED after about 128 calls in 20 seconds, and from then on it was allowed roughly 4 calls a second. Worse, a rate-limited query could come back with zero rows. If your code treats that as an empty team, the agent tells the user the team has no work.&lt;/p&gt;

&lt;p&gt;That's why twg() retries anything that looks transient and every page is checked for errors. The Jira REST calls in these actions also count against Jira's &lt;a href="https://leanzero.net/tutorials/surviving-atlassians-points-based-rate-limits-in-a-forge-app-2026?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;points-based rate limits&lt;/a&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Faaf9b56fe380c8d047d22b820397e33f5e636749-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Faaf9b56fe380c8d047d22b820397e33f5e636749-1600x896.png" title="Illustration, generated locally with ERNIE-Image. A rate-limited query can return zero rows while the data sits right there, about 128 calls into a 20-second burst in our tests. The bucket is empty and the pond is full." alt="Illustration: two orange robots fish with an empty bucket while, under the water behind a fence, a crowd of fish waits, and a lifeguard robot holds up a sign reading RATE LIMIT" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep paging while hasNextPage is true
&lt;/h3&gt;

&lt;p&gt;In our tests a page could come back short with hasNextPage still true. Stop on a short page and you can lose most of a list. So stop only when hasNextPage is false, and cap the number of pages yourself, as twgAll does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Missing scopes drop rows silently
&lt;/h3&gt;

&lt;p&gt;The scopes page says: "A missing scope doesn't produce an error ... A query that reads an object type you weren't granted comes back successful and empty." In Preview we also saw rows keep coming back after we removed a scope. The same page says scope changes take about a minute to apply, which might explain it. Either way, a query working without some scope tells you nothing, so declare one for every object type your query touches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rovo action response size and timeout
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://developer.atlassian.com/platform/forge/manifest-reference/modules/rovo-action/" rel="noopener noreferrer"&gt;action docs&lt;/a&gt; say an action "can only handle data up to 5 MB". In Rovo chat we got less. About 1 MB came back intact. About 1.1 MB failed with a 500. Rovo then retried with a smaller request. Rovo also gave up on an attempt after about 20 seconds, though the &lt;a href="https://developer.atlassian.com/platform/forge/limits-invocation/" rel="noopener noreferrer"&gt;invocation limits page&lt;/a&gt; gives an action function 55 seconds. I'd plan for well under 1 MB and well under 20 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Atlassian home chat may run a different installation
&lt;/h3&gt;

&lt;p&gt;When we opened Rovo chat from the Atlassian home page, one of our actions ran in a different installation of the app, and its app storage was empty. From the Rovo chat panel inside Jira it read the Jira installation's storage. Keep actions stateless, or read what you need from Jira each time, like this demo does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic auth: UNAUTHENTICATED is not allowed in service graph_store
&lt;/h3&gt;

&lt;p&gt;Want to poke at the API with curl and an API token first? You'll get partway. A trivial echo field answers. Anything that reads data returns IncorrectAuthException with "Auth category: UNAUTHENTICATED is not allowed in service graph_store". The schema download is the one thing that works with basic auth. For data, call the API from Forge or use OAuth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rovo on free developer sites
&lt;/h3&gt;

&lt;p&gt;Rovo is included with paid plans only, and the Rovo billing KB says "There are currently no plans to offer a free tier of Rovo". So on a free developer site you may not get the agent chat at all. Curated queries did answer on a free site in our tests; teams live at the org level, not on the site. If you're entering Codegeist, keep that in mind when you write testing instructions for judges. On a paid site, our page on &lt;a href="https://leanzero.net/tutorials/rovo-usage-limit-extra-usage?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Rovo usage limits and extra usage&lt;/a&gt; explains how the Rovo credit allowance and extra usage work from 3 December 2026. Before you put an agent like this in front of your team, Gabriela's &lt;a href="https://leanzero.net/blog/rovo-credits-per-user?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Rovo credits per user&lt;/a&gt; piece gives measured credit costs per action, so you can estimate what the chat will use. Once people are using it, her guide to &lt;a href="https://leanzero.net/tutorials/check-rovo-credits-usage-each-user?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;checking Rovo credits usage for each user&lt;/a&gt; shows how to export who used what, and where the 90-day limit on that export applies. And if you'd rather call the agent from a Jira Automation rule than from chat, read her &lt;a href="https://leanzero.net/tutorials/rovo-agent-pricing-jira-automation?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Rovo agent pricing in Jira Automation&lt;/a&gt; first, because it works out the cost per agent call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you built, and what production would add
&lt;/h2&gt;

&lt;p&gt;You now have a Jira Forge app with one Rovo agent and three read-only actions.&lt;/p&gt;

&lt;p&gt;[[takeaways]]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It answers "what am I working on", "what is this person working on", "who is involved in this work item" and "what is my team working on" from the Teamwork Graph, as the person asking.&lt;/li&gt;
&lt;li&gt;Every query is named, paged 25 rows at a time and retried on transient errors; partial reads come back as notes, which the agent reports as possibly incomplete.&lt;/li&gt;
&lt;li&gt;It needs eight scopes and an org admin to install, and it ran each action in 0.8 to 2.6 seconds on our test site.&lt;/li&gt;
&lt;li&gt;Not covered: permission filtering against a project the asker can't see (untested), goals (no goal was linked, so jiraWorkItemGoals returned 0 rows), caching, teams the asker isn't in, and more than one page of work per team member. Our test project had seven work items, so the demo never needed a second page.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next: rovo:mcp, persisted Cypher and Teamwork Graph connectors
&lt;/h2&gt;

&lt;p&gt;Add a &lt;a href="https://developer.atlassian.com/platform/forge/manifest-reference/modules/rovo-mcp/" rel="noopener noreferrer"&gt;rovo:mcp module&lt;/a&gt; and the same actions become tools for Model Context Protocol (MCP) clients. It's in Preview, you get at most one per app, and external exposure stays off until an admin turns it on (Gabriela's piece on &lt;a href="https://leanzero.net/blog/atlassian-mcp-server-permissions-data-security-policy?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Atlassian MCP server permissions&lt;/a&gt; is worth opening before you turn it on, because it shows what MCP clients can read and write once an admin allows them). Persisted Cypher covers traversals that curated queries don't. And Teamwork Graph connectors are the write side, for putting your own objects into the graph, which is the "connects data into the graph" half of the Teamwork Graph bonus wording.&lt;/p&gt;

&lt;p&gt;Building something on this for Codegeist? I'd start from twg.js as it is and change OP_PREFIX to your own company name.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/tutorials/atlassian-teamwork-graph-api-forge-rovo-agent?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;



</description>
      <category>jira</category>
      <category>forge</category>
      <category>atlassian</category>
      <category>ai</category>
    </item>
    <item>
      <title>Jira Planner vs Rovo Work: Atlassian AI planner</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Mon, 02 Nov 2026 05:59:34 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/jira-planner-vs-rovo-work-atlassian-ai-planner-16i</link>
      <guid>https://dev.to/mihai_leanzero/jira-planner-vs-rovo-work-atlassian-ai-planner-16i</guid>
      <description>&lt;p&gt;Jira Planner is Atlassian's AI planner. You give it one rough request, it asks clarifying questions, writes a Confluence plan and then breaks the approved plan into sequenced Jira work items. Atlassian opened early access in August, and at Team '26 Europe in Amsterdam in October it called Planner "private early access". Rovo Work, a new plan-first mode in Rovo Chat, was announced the same week without any status label. You can join a waitlist for both, but you can't switch either one on yourself today. When we asked Rovo Chat to plan one of our Jira projects on 1 November, it suggested 11 work items and told us 79 of the project's 88 existing items had no description.&lt;/p&gt;

&lt;p&gt;The two names are easy to mix up, and we mixed them up ourselves when we first sketched this article. Is Rovo Work the thing that writes the plan to Confluence, or is that Planner? It's Planner. Rovo Work, in Atlassian's words, "proposes a plan, you shape it, and it executes across Jira, Confluence, and your connected tools". None of the Atlassian pages we found says it writes that plan to a Confluence page.&lt;/p&gt;

&lt;p&gt;Work mode isn't in the Rovo Chat menu on our test site, and Planner is early access only, so we did the next best thing. On 1 November we asked Rovo Chat to plan a release audit for one of our real Jira projects. We told it to suggest the work items but not create them. It worked for 130 seconds and 20 steps. It came back with a 10-step plan and 11 work items, and then asked us 2 questions before it would create anything. Rovo called the project's missing fix versions the biggest gap. That project, LZPT, is a test bed and some of its oddities are deliberate test data, but I'd bet plenty of real projects have the same gaps: no fix versions and work items with no description.&lt;/p&gt;

&lt;p&gt;Atlassian says Planner pulls from your Jira work items and Confluence pages through the Teamwork Graph (Atlassian's index of the work data across your apps), and Rovo Work runs across Jira and Confluence too. That's the content Rovo Chat already reads, so the cleanup that gets you a decent plan out of Rovo Chat now is very likely the cleanup Planner will need later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six things we found: two different features, and a planner that's only as good as your Jira
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Planner and Rovo Work are two different features.&lt;/strong&gt; Planner writes a Confluence plan and turns it into Jira work items. Rovo Work is a mode inside Rovo Chat that plans and then runs the steps itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Planner's status has moved between three wordings.&lt;/strong&gt; "now available in Early Access" (19 August), "now open for Early Access" (4 September), then "Shipping Soon" and "private early access" (7 October). Atlassian hasn't announced general availability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rovo Work has no status word at all.&lt;/strong&gt; It has an announcement (6 October), a waitlist page and a line on the Rovo pricing page. None of the 20,629 English URLs in the support site's sitemaps is about it, and the support page comparing Rovo Chat's reasoning modes (updated 22 October) lists three modes, none of them Work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rovo Work looks like the renamed Max mode,&lt;/strong&gt; a plan-and-execute Rovo Chat mode Atlassian announced in May. The waitlist page's title still says Max mode, while its heading says Work mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rovo Chat already plans before it acts if you ask it to.&lt;/strong&gt; Our run took 20 steps, suggested 11 work items and stopped to ask 2 questions before it would create any of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The plan was only as good as the project under it.&lt;/strong&gt; Rovo flagged its own due dates as guesses, and most of what it found was missing data in the project.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What Jira Planner does: one request, a Confluence plan, sequenced Jira work items
&lt;/h2&gt;

&lt;p&gt;Atlassian's &lt;a href="https://www.atlassian.com/blog/jira/introducing-jira-planner" rel="noopener noreferrer"&gt;August post introducing Jira Planner&lt;/a&gt; describes six steps, and they're worth reading in order because the order is the point. You start with "an idea of goal, even a rough one" (that typo is Atlassian's). Planner asks follow-up questions about scope and what success looks like. It reads "your code across multi-repo setups" and "pulls in context from Teamwork Graph". It writes the plan as "editable Confluence Live Docs" (Confluence pages your team co-edits in real time). Your team co-edits it and leaves inline comments. Planner then checks "whether your spec is detailed enough for execution". Then, and only then, it breaks "the finalized plan into Jira work items, sequenced with dependencies and milestones, with each one carrying its own acceptance criteria and the context of the full plan."&lt;/p&gt;

&lt;p&gt;The October version of the flow comes from the Team '26 Europe post about the AI-native SDLC. It's on &lt;a href="https://www.atlassian.com/blog/company-news/team26-europe-ai-sdlc" rel="noopener noreferrer"&gt;Atlassian's blog&lt;/a&gt; and adds what goes into the plan: "objectives, options, recommended path, risks, dependencies, owners, and review questions that stakeholders can comment on and refine together." The work items it creates carry "descriptions, acceptance criteria, repo links, dependencies, and enough context for people and AI agents to execute safely."&lt;/p&gt;

&lt;p&gt;Atlassian says Planner is for "tech leads, senior engineers, engineering managers, and PMs". The work it has in mind is multi-sprint features, architectural decisions "and anything where more than one person needs to agree on scope". So it's a Jira feature for software teams first. I think a marketing team planning a campaign could use it later, but that's not who Atlassian wrote it for.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Ffb4054d19219c8882ff2d90173e9c43757fc2dec-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Ffb4054d19219c8882ff2d90173e9c43757fc2dec-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Planner writes and checks the plan before a single Jira work item exists, and the numbered boxes only move once the plan is approved. The small robots have been holding those boxes since August." alt="Illustration: two robots study a big blueprint labelled PLAN on a table, a third robot holds a rubber stamp over a box marked APPROVED, and four small robots wait in a line holding boxes numbered 1 to 4" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Spec-driven development in Jira: why Planner starts with questions
&lt;/h2&gt;

&lt;p&gt;Atlassian calls this spec-driven development. In plain words: agree on what you're building before anyone builds it. Its August post quotes its own survey. 60% of engineering leaders said their teams are moving that way, and "fewer than 15% have a structured framework to actually do it."&lt;/p&gt;

&lt;p&gt;So Planner asks before it writes. The October post lists the questions it's after: "scope, constraints, success criteria, and open decisions." The readiness check sits between the plan and the work items for the same reason. Atlassian says Planner "surfaces ambiguity upfront and tells you whether your spec is detailed enough for execution, with specific suggestions for what's missing."&lt;/p&gt;

&lt;p&gt;Atlassian's posts don't say yet who approves the plan. The October post says Planner "Breaks an approved plan into sequenced Jira work items", and the August one says "the finalized plan". None of Atlassian's Planner posts describes an approval control, a role or a button. If your change process needs a named approver, ask about it in the early access program. Don't assume it's there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rovo Work: the plan-first mode in Rovo Chat
&lt;/h2&gt;

&lt;p&gt;Rovo Work is announced in exactly one Team '26 Europe post, the &lt;a href="https://www.atlassian.com/blog/company-news/team26-europe-ai-platform" rel="noopener noreferrer"&gt;6 October one about the Atlassian platform&lt;/a&gt;. The definition is short, so here it is in full: "Work is a new mode in Rovo Chat that handles complex, multi-step tasks with human oversight to review and approve. You start with a goal, it proposes a plan, you shape it, and it executes across Jira, Confluence, and your connected tools. A task can run for hours, looping until it finds the right tools, teaches itself new ones, runs scripts, and completes the ask, all in a secure sandbox your admins govern."&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.atlassian.com/software/rovo/work-waitlist" rel="noopener noreferrer"&gt;waitlist page&lt;/a&gt; gives examples of what you'd ask it for: "a cross‑project report, a competitive brief, or a project plan". A project plan is one possible output. Mostly, though, Rovo Work is a general worker that plans first, and Planner is the one built around Jira.&lt;/p&gt;

&lt;p&gt;This next bit confused me for a while. The waitlist lives at two addresses, /software/rovo/max-waitlist and /software/rovo/work-waitlist. Both serve the same page. Its browser title is "Complex tasks, meet Max mode" and the heading on the page says "Complex tasks, meet Work mode". Its first sentence starts "Work mode is Rovo's new reasoning mode". Swap "Work" for "Max" and it's word for word the sentence in Atlassian's support doc for Max mode in Rovo Desktop. Max was announced on 6 May as "Coming soon in early access", and that announcement already promised "the plan to review and approve". We found no Atlassian page saying Max is now called Work, so treat this as a strong guess.&lt;/p&gt;

&lt;p&gt;One more thing to know about cost, as a practical note only. Atlassian's &lt;a href="https://www.atlassian.com/licensing/rovo" rel="noopener noreferrer"&gt;Rovo pricing page&lt;/a&gt; lists "Work mode in Rovo Chat" among Premium AI interactions with "Variable pricing per billable event", while basic interactions are 10 credits each. In &lt;a href="https://dev.to/blog/rovo-credits-cost-think-deeper"&gt;our Think deeper credits test&lt;/a&gt; a single message cost anything from 10 to 182 credits. A task that "can run for hours" is worth watching once it reaches you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rovo Work Item Planner, Jira Planner and Rovo Work: which is which
&lt;/h2&gt;

&lt;p&gt;There are three names here that sound alike, plus a fourth that admins already know, so here they are side by side.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Where you meet it&lt;/th&gt;
&lt;th&gt;Status (2 November 2026)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Work item Planner&lt;/td&gt;
&lt;td&gt;An out-of-the-box Rovo agent that breaks epics or pages into smaller tasks&lt;/td&gt;
&lt;td&gt;Rovo agents, in Jira or Chat&lt;/td&gt;
&lt;td&gt;Part of Rovo's built-in agents today&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jira Planner (Planner)&lt;/td&gt;
&lt;td&gt;Spec-driven planning: questions, a Confluence live doc plan, a readiness check, then sequenced work items&lt;/td&gt;
&lt;td&gt;Jira, with Confluence and the Teamwork Graph&lt;/td&gt;
&lt;td&gt;Early access via waitlist, onboarded in waves; "private early access" since 7 October&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rovo Work (Work mode)&lt;/td&gt;
&lt;td&gt;A Rovo Chat mode that plans, then runs multi-step tasks that can run for hours in an admin-governed sandbox&lt;/td&gt;
&lt;td&gt;Rovo Chat&lt;/td&gt;
&lt;td&gt;Announced 6 October, waitlist open, no status label or support doc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plans (Advanced Roadmaps)&lt;/td&gt;
&lt;td&gt;Jira's timeline planning across teams. Atlassian's weekly changelog listed a Rovo "Suggest child work items" action as rolling out from May to early August, then dropped it without saying why&lt;/td&gt;
&lt;td&gt;Jira Premium and Enterprise&lt;/td&gt;
&lt;td&gt;Existing feature; we found no support doc for the Rovo suggestion action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can try the Work item Planner agent right now. Its use-case page suggests prompts like "Break down my epic into subtasks." and "Turn this page into a list of tasks." Atlassian's descriptions of it don't mention a Confluence plan or a readiness check, and that's the main difference from Jira Planner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rovo Chat planned our Jira project before acting, and found 79 empty descriptions
&lt;/h2&gt;

&lt;p&gt;On our test site the Rovo Chat reasoning menu, on the night of 1 November, had three options: Let Rovo decide, Quick answers and Think deeper. No Work mode, no Max. (On 8 October, when we wrote up &lt;a href="https://dev.to/blog/atlassian-team-26-europe-announcements"&gt;the Team '26 Europe announcements&lt;/a&gt;, the menu also had Deep Research. It's not in that menu any more on our site.)&lt;/p&gt;

&lt;p&gt;So we used Think deeper. Atlassian describes it as Rovo spending "more time analyzing and planning before responding". The project was LZPT, the Jira project we use as a test bed for one of our own apps. We sent this prompt from Mihai's admin account: "Make a plan to get the LZPT project ready for a release audit next month. Show me the plan first as numbered steps with an owner and order for each, and suggest the Jira work items it would create, but do not create anything 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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F73b4ba3c53d1693dd9772fee1e992e2d1bbec0d7-1560x1210.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F73b4ba3c53d1693dd9772fee1e992e2d1bbec0d7-1560x1210.png" title="Rovo Chat in Think deeper mode on our test site, 1 November 2026. Before planning anything it audited the project: 88 work items, no fix versions, 33 overdue." alt="Rovo Chat answer on our test site: the prompt asking for a release-audit plan, &amp;quot;Completed 20 steps&amp;quot;, then a Current state of LZPT list: no releases and no fix versions on 88 work items, 77 To Do, 4 In Progress, 7 Done, and hygiene gaps such as 33 overdue items and 79 of 88 with no description" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It finished in 130 seconds. "Completed 20 steps" sat above the answer. The first line was reassuring: "I haven't created or changed anything." Then, before any plan, it described the project. These are Rovo's counts, and we didn't re-run them in JQL.&lt;/p&gt;

&lt;p&gt;None of the 88 work items has a fix version, and the project has no versions at all ("This is the biggest gap"). Every item is Medium priority and assigned to Mihai. 33 open items are overdue, with due dates between 1 May and 12 October 2026. Another 48 have no due date. 42 have no parent epic and 64 haven't been updated in over 30 days. And almost all of them, 79 of 88, have no description.&lt;/p&gt;

&lt;p&gt;It also spotted our deliberate test data (circular dependencies, and an item whose start date is after its due date, in epics E1 to E6). And it found the throwaway items, one of them with "safe to delete" in its title.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fd70f4ef65d120e52987a0ebf29b512df03ef593b-1380x1470.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fd70f4ef65d120e52987a0ebf29b512df03ef593b-1380x1470.png" title="The first four of the ten steps Rovo proposed. Each step has an owner and a week, and step 3 keeps our deliberate test fixtures by labelling them." alt="The plan table Rovo proposed: steps 1 to 4 with owner and week, starting with confirming the audit's scope, date and pass criteria in week 1 (2 to 6 November), creating release versions, labelling the test fixtures, and removing throwaway items in week 2" width="800" height="852"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The plan had 10 steps over five weeks, from confirming the audit's scope (2 to 6 November) to a freeze in the week of 30 November. Step 8 was the evidence pack: release notes in Confluence, a saved "audit scope" filter and a dashboard. Step 9 suggested me as the pre-audit reviewer. Then came the work items it would create. One epic, "Release audit readiness – Dec 2026", due 4 December. Under it, 10 tasks due between 6 November and 4 December.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F4f67d36687882f34cefc16b9afcc23cd7afdcc41-1380x1525.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F4f67d36687882f34cefc16b9afcc23cd7afdcc41-1380x1525.png" title="Rows A to G of the eleven work items. Look at the Parent column: every task points at epic A and has its own due date. Planner promises more, with dependencies and milestones built from an approved Confluence plan." alt="Table of work items Rovo would create: A is an Epic, Release audit readiness Dec 2026, due 4 December; B to G are Tasks under it such as Confirm audit scope, Create release versions, Label harness fixtures, Delete or archive throwaway items, Sort out 33 overdue open items, with due dates from 6 to 13 November" width="800" height="884"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And then it stopped. I liked that. "Before I create anything, I need two answers from you", it wrote. The first was the audit date: "I assumed early December, so every due date above is a guess." The second was whether I should really be the reviewer, since "Gabriela isn't currently assigned to anything in LZPT." It also noticed that LZPT's description calls it a test harness, "Safe to delete". A real audit, it said, might be better off in a dedicated release project. Fair point. We didn't click create, because LZPT is our test bed and we want it messy.&lt;/p&gt;

&lt;p&gt;So how close is this to Planner? On the order of things, closer than I expected: it looked first, proposed, asked, and only then offered to create. It's missing two things Planner promises, though. The plan lives in the chat, where your team can't comment on it the way they would on a Confluence live doc. And there's no readiness check, only Rovo's own note that the dates are guesses. This was one run on one project with one prompt, so please read it as a demo. It's not a benchmark. We didn't measure its credits either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jira Planner and agents: how an approved plan hands work to people and coding agents
&lt;/h2&gt;

&lt;p&gt;Atlassian is clear that the work items Planner creates are meant for both humans and agents. The August post says that after the plan becomes work items, "agent orchestration takes over, picking up exactly where planning left off." The October post lists Planner next to agent loops and AI Review. All three (agent loops turn backlog items into pull requests, AI Review reviews code) share one availability line: "available in private early access."&lt;/p&gt;

&lt;p&gt;What does an agent picking up a Jira work item look like today? Rovo agents can already be assigned work items, mentioned in a comment, triggered by a workflow transition or a board column, and that experience is "currently in beta". In automation they can run without anyone confirming each action, but the "Let this agent take actions" toggle is off by default, including on existing rules. In interactive use, Atlassian's best-practice page says "work items won't be updated, pages won't be edited, and emails won't be sent until a human approves them."&lt;/p&gt;

&lt;p&gt;There are known gaps on today's path too. A &lt;a href="https://jira.atlassian.com/browse/ROVO-704" rel="noopener noreferrer"&gt;public Atlassian ticket, ROVO-704&lt;/a&gt;, describes an agent that creates epics and stories but "fails to create Tasks/Sub-tasks on the first attempt". The reason given is that the bulk create API can't create sub-tasks in the same call as their parent stories. It has 6 votes. And a Community thread with 2,335 views reports agent-created items coming out with an empty description unless you open "Edit Detail". Planner promises acceptance criteria on every item. I'd like to see whether it gets past those gaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Early access status: who can use Jira Planner and Rovo Work today (only the waitlist)
&lt;/h2&gt;

&lt;p&gt;For Planner, only teams Atlassian has onboarded from its waitlist. For Rovo Work, there's a waitlist, and we couldn't find anyone who says they have it yet.&lt;/p&gt;

&lt;p&gt;For Jira Planner, the &lt;a href="https://jirareleases.atlassian.com/announcements/jira-planner-early-access-is-now-open" rel="noopener noreferrer"&gt;4 September Jira release note&lt;/a&gt; says it "is currently available in Early Access for eligible Jira Cloud customers. You will need Rovo enabled, Teamwork Graph connected, and Confluence available on the same site." Atlassian is "onboarding teams in waves". The waitlist sits on &lt;a href="https://www.atlassian.com/software/jira/dev/waitlist" rel="noopener noreferrer"&gt;Atlassian's Jira page for new AI development features&lt;/a&gt;, with four other programs. The Community announcement from August has 1,231 views and 3 comments. Two of them say "very informative" and "Great", and the third has been deleted, so there's very little public feedback to go on.&lt;/p&gt;

&lt;p&gt;Rovo Work's waitlist form asks what you'd use it for, with six choices from research and analysis to work management.&lt;/p&gt;

&lt;p&gt;What I couldn't find for either one: a plan tier requirement, a price for Planner, a date for general availability, or a single support article. Atlassian's beta switch for Rovo is on by default (you'll find it under AI and Rovo in Atlassian Administration). Atlassian also warns that "even if you've disabled beta features for your organization, some early access experiences may still be accessible." So don't count on that switch to keep an early access feature away from your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to get ready for Atlassian's AI planner: Teamwork Graph, Confluence and admin controls
&lt;/h2&gt;

&lt;p&gt;This is what I'd do this month, in this order. Step 1 is Atlassian's stated requirement. Steps 2 and 3 follow from how Atlassian says Planner works, though it doesn't list them as requirements. Step 4 is my opinion from our run, and steps 5 to 7 are my suggestions based on Atlassian's docs.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check the three prerequisites.&lt;/strong&gt; Rovo enabled, Teamwork Graph connected, and Confluence on the same site as Jira. Not sure what the Teamwork Graph links up on your site? Our &lt;a href="https://dev.to/blog/what-is-atlassian-teamwork-graph"&gt;Teamwork Graph explainer&lt;/a&gt; shows what it connects and what we saw with zero connectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch on live docs where plans should land.&lt;/strong&gt; Atlassian doesn't say Planner needs this per space, but plans are live docs, and live docs "can be turned on or off in any given space by admins." A new live doc is "immediately visible in the sidebar, and anyone with access to the space can see it". So pick a space whose audience is fine seeing a draft plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide whether code is in scope.&lt;/strong&gt; Planner reads code across repositories, and the October post names Code Context as one of its sources. Code Context is off by default, with one exception: when a paid Bitbucket workspace is connected to a Jira site with Rovo on, Atlassian turns it on automatically and indexes that workspace. The index is stored in the United States, and data residency isn't supported for it. If you use Bitbucket, check whether it's already on, and let whoever owns data residency in your company make the call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give your work items descriptions and parents.&lt;/strong&gt; This one is my opinion and you won't find it in Atlassian's docs. Rovo built its plan from what it found, and on LZPT that was mostly empty descriptions and open items with no parent epic. A planner that groups work by epic and writes acceptance criteria will only be as good as what it reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Settle your work type hierarchy before you need it.&lt;/strong&gt; Jira's default has three levels (epic, story, subtask), and Premium and Enterprise sites can add more. Atlassian's warning is blunt: changing the hierarchy "will break existing parent and child relationships" and "cannot be undone", and it applies to all company-managed spaces. Do that thinking now. The week Planner starts creating parent items is too late.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name who governs Rovo Work.&lt;/strong&gt; Atlassian says the sandbox is one "your admins govern", but there's no doc on those controls yet. Agree internally who that admin is. Meanwhile, look at the agent controls you do have today. Agent creation is open to all users by default, and you can limit it to up to 10 groups. And if no agent account is set up, agents act as the person who triggered them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try today's planning tools on one real project.&lt;/strong&gt; In Jira, select Ask Rovo and type /create-work-items followed by what you want. Rovo lists suggested items in the chat; select Preview to open them in a canvas, edit them, then select Create all. It needs beta AI features on and it's rolling out gradually. In the list view, "Break down with Rovo" (also still rolling out) suggests child items for a parent. Or do what we did: ask Rovo Chat for a plan first and the items second.&lt;/li&gt;
&lt;/ol&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F9bb6b95cc8b357e00ac8267acb4ae14c7d443384-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F9bb6b95cc8b357e00ac8267acb4ae14c7d443384-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Rovo plans from what your work items say, and most of ours said nothing. The robot on the right wrote one description and won't stop talking about it." alt="Illustration: a frowning robot surrounded by question marks reads a pile of blank notebooks and empty folders, while a smiling robot next to it holds one neat labelled yellow folder" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So should you join the waitlist?
&lt;/h2&gt;

&lt;p&gt;If you run Jira for software teams who already plan big features in Confluence, yes, join the Jira Planner waitlist. While you wait, do steps 1 to 3 on your site and steps 4 and 5 on one project. Anyone with access to the space can see the plans it writes, and people and agents will pick up the items it creates. A tidy project probably decides whether you edit that first plan or throw it away.&lt;/p&gt;

&lt;p&gt;If your interest is Rovo Work, join that waitlist too, but sort out step 6 first. Your admins will want a say in a task that can run for hours in a sandbox and write to Jira and Confluence. Atlassian hasn't published how admins will govern it yet.&lt;/p&gt;

&lt;p&gt;Have you got into either early access program yet? I'd love to hear what Planner asked you before it wrote its first plan, or whether Rovo Work actually showed up in your Rovo Chat menu. Tell me on the Atlassian Community or at &lt;a href="mailto:gabriela@leanzero.net"&gt;gabriela@leanzero.net&lt;/a&gt; :)&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/blog/jira-planner-rovo-work?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>jira</category>
    </item>
    <item>
      <title>What is the Atlassian Teamwork Graph? What it connects in Jira, Confluence and other tools</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Sat, 31 Oct 2026 12:01:06 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/what-is-atlassian-teamwork-graph-connectors-dke</link>
      <guid>https://dev.to/mihai_leanzero/what-is-atlassian-teamwork-graph-connectors-dke</guid>
      <description>&lt;h1&gt;
  
  
  What is the Atlassian Teamwork Graph? What it connects in Jira, Confluence and other tools
&lt;/h1&gt;

&lt;h3&gt;
  
  
  Key takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Teamwork Graph is Atlassian's data layer that links your Jira work items and Confluence pages to people and to third-party content. You never open it directly. You see it through Rovo, which comes with any Standard plan or higher of Jira, Confluence, Jira Service Management or one of the two collections.&lt;/li&gt;
&lt;li&gt;Atlassian's own size figures disagree: 'over 150 billion objects and relationships' in May 2026, '250B+ connections' on 6 October 2026, and again 'more than 150 billion objects and relationships' on 16 October. The connector count is '80+' in the October blogs, 35 setup pages plus 31 Smart Link apps on the support doc, and 100 on a 2025 developer page.&lt;/li&gt;
&lt;li&gt;On our test org with 0 third-party connectors, Rovo Chat answered a cross-app question in 71 seconds: it tied a Jira project of 88 work items to 2 Confluence pages in a different space. A follow-up took 81 seconds and 9 visible tool steps. Its narration names a CQL search (Confluence's query language), so the test shows what users see but doesn't prove which links came from the graph.&lt;/li&gt;
&lt;li&gt;Permissions follow the source app: a private Google Drive file shows only to people who can open it in Drive, and an org admin uninstalling a connector deletes its content from the graph within 30 days (GitHub and Workday are exceptions). Smart Link connectors can't be switched off by an admin; the request for that, ROVO-183, has 95 votes.&lt;/li&gt;
&lt;li&gt;Team '26 Europe added code (code context in open beta, plus a Rovo Code Search app with no separate status) and structured data (Atlassian Insights, phased rollout) as context types. Our admin page says code context turns on automatically for Rovo-connected workspaces by January 2027, and Atlassian's setup doc says indexed code is stored in the United States.&lt;/li&gt;
&lt;li&gt;Our Connectors admin page also shows a banner I couldn't find in any public Atlassian doc: from 15 November 2026, 'Related Objects Indexing' will index the content behind user-authorized third-party links inside your Atlassian apps. Read it now if you care about what ends up in search.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Back in April someone asked on the Atlassian Community, about the SharePoint connector, "where is the data stored? Is it actually storing a copy of the documents". It's a fair question once you realise Rovo (Atlassian's AI assistant and search) can answer from files that don't live in Atlassian at all. The answer is yes. Connectors bring in content from tools outside Atlassian. For the connectors that sync, Atlassian copies the workspace you connect into something called the Teamwork Graph. Atlassian's own example is "the entire Google Drive or the SharePoint workspace".&lt;/p&gt;

&lt;p&gt;So what is it? The Teamwork Graph is the map Atlassian keeps of how your work fits together, meaning which work items and pages relate to which people and connected tools. There's nothing to install or open. Rovo reads it. On 6 October 2026 Atlassian said it holds "250B+ connections". In May it said "over 150 billion" objects and relationships (and a 16 October Atlassian blog still says 150 billion, so quote whichever you use with its date).&lt;/p&gt;

&lt;p&gt;We tried it on our own test org with zero third-party tools connected. Rovo tied a Jira project to two Confluence pages that had no links to it, in 71 seconds. Its step log names a CQL search (Confluence's query language) and a page read. A plain text search can find those pages without any graph link, so I can't tell you how much of the answer came from the graph itself. I can show you exactly what we saw.&lt;/p&gt;

&lt;p&gt;The part I'd act on is in Atlassian Administration. Two pages in Atlassian Administration (admin.atlassian.com, where org admins manage connectors) carry dates. From 15 November 2026, Atlassian will index the content behind third-party links people have authorized. By January 2027, code context, which indexes your source code, turns on automatically. I couldn't find either date in Atlassian's public docs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five things the docs and our test org show
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Atlassian's numbers don't agree with each other.&lt;/strong&gt; "Over 150 billion" objects and relationships (6 May 2026), "250B+ connections" (6 October 2026), "more than 150 billion" objects and relationships (16 October 2026). Connectors: "80+" in the October blogs, 35 setup pages on the support doc, 100 on a 2025 developer page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rovo can connect Jira work to Confluence pages with no connectors at all.&lt;/strong&gt; With 0 connectors, Rovo Chat tied a Jira project of 88 work items to 2 Confluence pages that had no links to it, through a CQL search and a page read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Permissions follow the source app.&lt;/strong&gt; Restricted third-party content shows only to people who can open it there, and uninstalling a connector deletes its content within 30 days, with exceptions for GitHub and Workday.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code context, where Rovo indexes your source code, is off on our org today.&lt;/strong&gt; The admin page says it turns on automatically for Rovo-connected Bitbucket workspaces by January 2027. Atlassian's setup doc says indexed code is stored in the United States.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A new indexing change starts on 15 November 2026.&lt;/strong&gt; Our Connectors page announces "Related Objects Indexing", which will index the content behind third-party links people have authorized inside Atlassian apps. I couldn't find a public doc for it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What is the Atlassian Teamwork Graph? A map of links that Rovo reads
&lt;/h2&gt;

&lt;p&gt;Atlassian's developer docs call it "Atlassian's unified data layer that connects teamwork data from across Atlassian apps, like Jira and Confluence, as well as external tools", built on "a common data model for work items, documents, messages, users, groups, projects, and more". The May launch post puts it more warmly: it stitches together "people, goals, code, and content" and becomes "your enterprise's living map of how work actually happens".&lt;/p&gt;

&lt;p&gt;In practice you'll never see a screen called Teamwork Graph. Atlassian says users "experience its value through features like unified search, intelligent recommendations, cross-tool analytics". So if you've used Rovo Search or Rovo Chat, you've used it. Atlassian's Rovo pricing page says Rovo is "Powered by the Teamwork Graph". Atlassian's Teamwork Collection blog from 7 October 2026 goes further and says the whole collection "is built on a shared context layer called the Teamwork Graph".&lt;/p&gt;

&lt;p&gt;Atlassian's AI planner, Jira Planner, lists Teamwork Graph connected as one of its requirements, and I wrote up what it does and how to get ready for it separately: &lt;a href="https://leanzero.net/blog/jira-planner-rovo-work?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Jira Planner and Rovo Work: what Atlassian's AI planner does&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Rovo comes with any Standard plan or higher of Jira, Confluence, Jira Service Management or one of Atlassian's two bundles, the Service Collection and the Teamwork Collection (Jira, Confluence, Loom and Rovo). You have the graph on every one of those sites, whether or not you connect anything else.&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%2F9klox15pah8u3hxna3o9.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%2F9klox15pah8u3hxna3o9.png" alt="Illustration: a big blue robot librarian with a GRAPH name badge stands among bookshelves while glowing strings join books, sticky notes, scrolls and film reels, and a small robot holding one book looks up at the web" width="800" height="448"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Illustration, generated locally with ERNIE-Image. Atlassian describes the graph as items plus the links between them, 250B+ connections by its October count. The small robot only came in for one book.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Teamwork Graph connects in Jira and Confluence
&lt;/h2&gt;

&lt;p&gt;Atlassian's developer docs list four kinds of link. Canonical links are structural, like a work item in a project or a page in a space. Activity links record "who did what to which objects", and Atlassian generates them "when users interact with objects". Logical links are umbrella links that cover several underlying link types, so they can be queried as one. And inferred links are "derived by Teamwork Graph itself", by "analyzing patterns, metadata, or using machine learning models on existing data".&lt;/p&gt;

&lt;p&gt;Atlassian doesn't say which of these links exist between a given page and a given work item, and you can't see them from the UI. On the Atlassian side the graph also covers Goals, Projects, Loom, Bitbucket and, since Team '26 Europe, Assets in Jira Service Management ("every asset, CI, or custom object is linked to the people, teams, and changes around it").&lt;/p&gt;

&lt;p&gt;It has gaps too. A Community post in May found a good one: "Space membership is not a first-class, queryable object in the Teamwork Graph today." Ask Rovo who the members of a Confluence space are and the graph has nothing to give it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where you see the Teamwork Graph working today: Rovo Search and Chat
&lt;/h2&gt;

&lt;p&gt;We tried it on a site with no extra connectors. Our test org, wolfaenpak, has a Jira project called LZ PPM Harness Test (key LZPT). My colleague Mihai Perdum uses it to test our planning app. It also has a Confluence space called WORK FOR HIRE with two plan-review pages that mention LZPT. Nobody linked those pages to the Jira work items. The LZPT keys sit in them as plain text.&lt;/p&gt;

&lt;p&gt;We searched for "LZPT" in Rovo Search on 31 October. Its results page has filter tabs for Jira, Confluence, Projects, Goals, Bitbucket, Jira Service Management and Assets. Above the Jira results sits an AI overview that had already joined the two products: "LZPT is a project key used in Jira and Confluence for a plan titled LZPT Scenarios", with the plan's status ("11% complete with 92% of the time elapsed, and 25 tickets were past due") and a link to the Confluence page it came from.&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%2Fx59p6isaszwikj5syedo.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%2Fx59p6isaszwikj5syedo.png" alt="Rovo Search results for LZPT: filter tabs for Jira, Confluence, Projects, Goals, Bitbucket, JSM and Assets, an AI overview naming the LZPT Scenarios plan with its status and a link to a Confluence page, then a list of Jira tasks from LZ PPM Harness Test" width="799" height="530"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Rovo Search on our test org, 31 October 2026. Notice the overview takes its status line from a Confluence page while the results under it are Jira tasks, with no connector set up.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then we asked Rovo Chat from Mihai's admin seat, in Quick answers mode (Rovo Chat's quick mode, as opposed to Think deeper): "What is the LZPT Scenarios plan, who is working on it, and which Confluence pages and Jira work items are linked to it?" It answered in 71 seconds. It found the project and its 88 work items. It said all 88 are assigned to Mihai, with me as reporter on a few. It sorted them into 12 scenario groups and found "Two pages in the WFH space reference this plan" (WFH is the key of the Confluence space those pages live in).&lt;/p&gt;

&lt;p&gt;Our follow-up asked about the Confluence side only, and you can watch the steps. It took 81 seconds and showed 9 completed steps. The narration in between says what it did. First: "I already have meaningful excerpts from the CQL search results". Then: "The full page body wasn't accessible via URL read". So it said it would fetch the page "via REST", Confluence's web API, next. Rovo quoted both pages correctly: "25 tickets past due, with only 11% done and 92% of the time gone", "44 of 79 tickets have no dates", and a dry note that "The planned finish of 2026-10-12 has already passed (today is 31 Oct 2026)".&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%2F9wazds57nobk5fujhxho.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%2F9wazds57nobk5fujhxho.png" alt="Two Rovo Chat answers side by side: on the left an overview of the LZPT Scenarios plan with who is working on it, on the right the step log for the Confluence question with completed steps and narration about a CQL search and a REST read before the answer" width="800" height="236"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Both chat answers from our test org. Notice the right-hand step log: Rovo says it found the pages with a CQL search and then went to read them over REST, 9 steps in 81 seconds.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So the result was right. The route to it was mostly search. A text search finds those pages without any graph link, so this test doesn't prove the graph made the connection. It shows what your users will actually see. We also didn't test a restricted page this time. Mihai's admin account can open everything in both spaces, so the test says nothing about permissions. One small puzzle: Rovo counted 88 work items in the project, while one of the Confluence pages counts 79 tickets in the plan. My guess is that the plan covers part of the project, but neither says so.&lt;/p&gt;

&lt;p&gt;In May Atlassian also announced in-product "expressions" built on the graph: "Related work", "People signals" and "Decision trails", showing up "across Jira, Confluence, Loom, and Rovo experiences". I couldn't find a support page or a beta or GA label for any of them, so don't plan around them yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teamwork Graph connectors: "80+" announced, 35 with setup pages
&lt;/h2&gt;

&lt;p&gt;Connectors are how content from outside Atlassian gets in, and this is where the "is it a copy?" question gets its full answer. Atlassian's support docs (updated 27 October 2026) describe three kinds. The third, Smart Links, are the preview cards Atlassian shows when you paste a link to another tool.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Connector type&lt;/th&gt;
&lt;th&gt;Is content stored by Atlassian?&lt;/th&gt;
&lt;th&gt;Who sets it up&lt;/th&gt;
&lt;th&gt;Can an admin switch it off?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Synced&lt;/td&gt;
&lt;td&gt;Yes, imported and indexed&lt;/td&gt;
&lt;td&gt;Org admin&lt;/td&gt;
&lt;td&gt;Yes, by disconnecting or uninstalling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct (Outlook Mail, Gmail, Slack)&lt;/td&gt;
&lt;td&gt;No, "retrieved live", "not stored or indexed by Atlassian"&lt;/td&gt;
&lt;td&gt;Org admin&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smart Link&lt;/td&gt;
&lt;td&gt;Partly: Atlassian indexes "only the links a user has seen in Atlassian surfaces that load as Smart Links"&lt;/td&gt;
&lt;td&gt;"Don't need to be setup by an admin"&lt;/td&gt;
&lt;td&gt;No, they "can't be disabled by an Admin"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The list on that page has 31 connectors an org admin sets up: Adobe Sign, Aha!, Airtable, Asana, Azure DevOps, Box, Confluence Data Center, Custom website, Docusign, Dropbox, GitHub, GitLab, Gmail, Google Calendar, Google Drive, Jira Data Center, Lucid, Outlook Calendar, Outlook Mail, SharePoint (with OneDrive), Microsoft Teams, Miro, Monday, Notion, Salesforce, ServiceNow, Slack, Smartsheet, Workday, Zendesk, Zoom. The same doc links setup pages for 4 more (Microsoft Entra, Google Workspace Directory, HRIS S3 data and a separate OneDrive page). That makes 35 connectors with setup pages. A separate table lists 31 Smart Link apps.&lt;/p&gt;

&lt;p&gt;So where does "80+" come from? Atlassian's 6 October platform post says "We've expanded our library of 80+ Connectors with additions like Zoom, Gong, Microsoft Entra ID, and Google Identity", plus "65+ improvements". A 2025 developer page says "100 out-of-the-box" connectors, and a May Community post said 75+. No Atlassian page reconciles these, so I'd plan from the support list. Zoom and Microsoft Entra, two of the four new names, have setup pages. Gong doesn't yet. I found nothing called Google Identity either; the nearest is a "Google Workspace Directory" connector that indexes people data, and maybe that's it.&lt;/p&gt;

&lt;p&gt;You can also build your own on Forge, Atlassian's app platform: Forge-built Teamwork Graph connectors became generally available in May 2026. Be careful with third-party ones, though. Atlassian's evaluation page says their capabilities "are self declared by the developer or vendor who created the connector and they're not verified by Atlassian".&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%2F908mromheqwu2ilgjyli.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%2F908mromheqwu2ilgjyli.png" alt="The Connectors page in Atlassian Administration on our test org: the intro text about indexing in the Teamwork Graph, a banner announcing Related Objects Indexing from Nov 15th 2026, and a card saying you've connected 0% of your team's apps" width="800" height="381"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Atlassian Administration, Connectors, on 31 October 2026. Notice the banner about Related Objects Indexing starting 15 November 2026, and the 0% card: our org has no connectors.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed at Atlassian Team '26 Europe: code context and data context
&lt;/h2&gt;

&lt;p&gt;Atlassian's main Team '26 conference in May 2026 was about opening the graph up to outside tools: the Teamwork Graph CLI (Open Beta then, and the page banner now says it's GA), Teamwork Graph tools in the Rovo MCP Server (MCP lets outside AI tools connect; Open Beta), Forge connectors (GA), and TeamworkGraph.com. If you want the whole Amsterdam event in one place, our &lt;a href="https://dev.to/blog/atlassian-team-26-europe-announcements"&gt;Team '26 Europe announcements roundup&lt;/a&gt; has the status of each item.&lt;/p&gt;

&lt;p&gt;Team '26 Europe, on 6 and 7 October 2026, added two new kinds of context, meaning information Rovo can draw on. The first is code. "The Teamwork Graph now reads your source code itself, down to functions, symbols, and classes", Atlassian wrote, with a new Rovo Code Search app across Bitbucket and GitHub. Atlassian's status line on 7 October covers code context, and doesn't name the app: "Code Context is gradually rolling out to paid Atlassian customers through open beta."&lt;/p&gt;

&lt;p&gt;The second is structured data. "Structured data is now a Teamwork Graph context type", and Atlassian Insights, which evolves the Analytics app, is its home, "curating 20+ structured sources with catalog, glossary, and lineage". Atlassian says Insights "will be available to all paid Atlassian Cloud customers, with a phased rollout starting after Team '26 Europe". Atlassian is careful to say what it brings in "isn't a copy of every row; it's governed metadata".&lt;/p&gt;

&lt;p&gt;Separately from the event, Connectors has had its own top-level entry in Atlassian Administration since 25 August 2026. And the Atlassian MCP server, which lets outside AI tools read your Jira and Confluence with each user's permissions, went GA in October. We tested what it can reach in &lt;a href="https://dev.to/blog/atlassian-mcp-server-permissions-data-security-policy"&gt;MCP server permissions vs data security policy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Teamwork Graph connectors in Atlassian Administration
&lt;/h2&gt;

&lt;p&gt;Only an organization admin can set up synced connectors, from admin.atlassian.com, then Connectors, All connectors, Add connector. Its wizard starts by asking for a site, and the connector belongs to that site. Most connectors connect "to a single workspace" per site, apart from Custom website and Jira Data Center. Atlassian's connector evaluation page lists Government Cloud as "Not available".&lt;/p&gt;

&lt;p&gt;Permissions come up first in every thread. Atlassian's rule is short: "The Teamwork Graph relies on and respects the permissions that are set in your third-party apps." The permission-sync doc gives examples. A private file in Google Drive "can only be seen and used in Rovo by those users who already have access to that content". If someone adds restrictions to a document in the connected app, Rovo search hides it from everyone else. If they remove the restrictions, it becomes "Visible in Search for everyone using Rovo on your site". Atlassian says it "listens" for permission changes. People may also have to connect their own account to an app before they see its content.&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%2F7tlq2tyx9ykc1salqi14.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%2F7tlq2tyx9ykc1salqi14.png" alt="Illustration: a blue robot bouncer holding a clipboard labelled PERMISSIONS and a badged robot pass a glowing folder over a velvet rope, while small robots on both sides watch, two of them holding empty trays" width="800" height="448"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Illustration, generated locally with ERNIE-Image. Rovo hands over only what your account could already open in the source app. The robots with empty trays were told it's nothing personal.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Deleting works like this. If an org admin uninstalls a third-party product from Atlassian Administration, "we will delete the content stored in Teamwork Graph within 30 days". GitHub is one exception: its data leaves Atlassian experiences only after you disconnect GitHub and also uninstall the GitHub for Jira app. Workday is the other exception. After you uninstall it, Atlassian's Talent app keeps its copy of the Workday data until you delete it from inside Talent. The Teams app deletes its copy too, but not if you had stopped syncing Workday to Teams before you uninstalled.&lt;/p&gt;

&lt;p&gt;For residency, our Connectors page says "indexed data follows your data residency requirements, stays within Atlassian's infrastructure, and is never processed by external AI models", and the residency doc lists "Ingested third-party(3P) content data" as in scope for Rovo. Code is the stated exception: "Data residency configuration isn't currently supported for code context. Indexed code and embeddings are stored and processed in the United States."&lt;/p&gt;

&lt;p&gt;Planning SharePoint first? We went through its consent screens in &lt;a href="https://dev.to/blog/rovo-sharepoint-connector-permissions"&gt;our SharePoint connector walkthrough&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What admins should check before connecting more tools
&lt;/h2&gt;

&lt;p&gt;All of this sits in admin pages you already have. I'd go through them in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Connectors page banner.&lt;/strong&gt; Ours says that "Starting Nov 15th 2026, Atlassian will discover and index content of user-authorized third-party links embedded within your Atlassian apps and connected third-party tools", with permissions synced to the source, and to "Contact Support with questions or to disable this feature". I found no public doc for it. As I read it, it works like this. Once someone has connected their Google or Figma account, a Google Doc or Figma link in a Jira work item could have the file's content indexed into the graph. Search and Rovo answers could then use it, and it would show only to people allowed to open it in the source app. Take a screenshot of your banner and decide before the 15th.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code context.&lt;/strong&gt; On our org the page says it "will turn on automatically for Rovo-connected workspaces by January 2027". Atlassian's two support docs disagree on whether it's "off by default" or auto-enabled for paid Bitbucket workspaces. Our page fits both. Code context is off today and turns on automatically by January 2027. If US storage of indexed code is a problem for you, raise it now. Atlassian says that if an admin disables it, "All stored embeddings, indices, and code chunks are deleted."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Link connectors.&lt;/strong&gt; You can't switch them off. Atlassian's feature request for admin control, &lt;a href="https://jira.atlassian.com/browse/ROVO-183" rel="noopener noreferrer"&gt;ROVO-183&lt;/a&gt;, is in Gathering Interest with 95 votes and 85 watchers; vote if it matters to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beta Rovo features.&lt;/strong&gt; They're on by default for all plans. An org admin can turn off "Enable beta features", though "Not all features are controlled by this setting." If you're weighing whether to switch Rovo off altogether, we listed what stays on in &lt;a href="https://dev.to/blog/disable-rovo-jira-confluence"&gt;Disable Rovo in Jira and Confluence&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocklists for big workspaces.&lt;/strong&gt; Google Drive and SharePoint let you exclude parts of the workspace with a blocklist. Without one, Atlassian imports "the entire Google Drive or the SharePoint workspace", so decide what to exclude before you connect.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On cost, the practical note is short. Atlassian says "storing indexed objects does not use Rovo credits". Each plan has a per-user allowance of indexed objects, the items connectors copy into the graph, pooled across your org (100 per user on Standard Jira, 2,500 on a Premium Teamwork Collection, for example). Asking Rovo questions does use credits, and we measured how many per action in &lt;a href="https://dev.to/blog/rovo-credits-per-user"&gt;Rovo credits per user&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  For developers: the Teamwork Graph API, CLI and MCP
&lt;/h2&gt;

&lt;p&gt;You don't need any of this to use the graph, but it's how other tools get at it. The Teamwork Graph API has been in Preview since 15 October 2026, results are "permission-filtered for the authenticated user", and installing an app with Teamwork Graph scopes needs an organization admin. The Teamwork Graph CLI and the Atlassian MCP server give AI coding tools the same permission-aware access from outside Atlassian. Installing and connecting them is free. Calls that pull Teamwork Graph context out through them (Atlassian calls these enriched calls) use Rovo credits.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what should you do with it?
&lt;/h2&gt;

&lt;p&gt;If you're on a Standard, Premium or Enterprise plan of Jira, Confluence or JSM Cloud, you already have Rovo, and Rovo reads the Teamwork Graph. The quickest way to understand it is the one we used: pick a project key or a plan name your teams use in both Jira and Confluence, search it in Rovo, then ask Rovo Chat how the two connect and open the step log. You'll see in about a minute what it links and what it misses.&lt;/p&gt;

&lt;p&gt;Before you add connectors, read the banner on your Connectors page and the code context page, because both have dates on them (15 November 2026 and January 2027). Then connect the one tool your people search most, and check a restricted file from a test account before you roll it out to everyone (we haven't run that check yet ourselves).&lt;/p&gt;

&lt;p&gt;I'm curious what you found when you tried it. Did Rovo connect something you didn't expect, or miss something obvious, like the space members it can't see? Tell me on the Atlassian Community or at &lt;a href="mailto:gabriela@leanzero.net"&gt;gabriela@leanzero.net&lt;/a&gt; :)&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/blog/what-is-atlassian-teamwork-graph?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>atlassian</category>
      <category>jira</category>
      <category>confluence</category>
      <category>rovo</category>
    </item>
    <item>
      <title>Jira Premium vs Enterprise Pricing at 200-500 Users (2026)</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Sat, 31 Oct 2026 06:01:52 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/jira-premium-vs-enterprise-pricing-at-200-500-users-2026-4745</link>
      <guid>https://dev.to/mihai_leanzero/jira-premium-vs-enterprise-pricing-at-200-500-users-2026-4745</guid>
      <description>&lt;p&gt;Jira Premium vs Enterprise pricing looks unanswerable below 801 users, because Atlassian's calculator won't show you an Enterprise number until you type 801. Set it to 350 users and the Enterprise column says "Enterprise plan pricing is available when you enter 801 or more users above", with a Contact sales button (it did on 30 October). But the same page carries Atlassian's whole price list as JSON, and that list prices Enterprise from 1 user up.&lt;/p&gt;

&lt;p&gt;So I priced it. At 200 users Jira Enterprise is $52,000 a year against $34,500 for Premium. At 350 it's $86,500 against $57,000, and at 500 it's $102,000 against $67,000. Per seat per month that is $21.67 against $14.38, $20.60 against $13.57, and $17.00 against $11.17. Enterprise costs about one and a half times Premium. It's the same ratio at 801 users, where Atlassian does print the price.&lt;/p&gt;

&lt;p&gt;I think most 200 to 500 seat companies that ask about Enterprise want one of four things: SSO through Guard, a second Jira site, faster support, or a bigger Rovo allowance before usage billing starts on 3 December. SSO and extra Rovo credits have list prices, and they cost less bought separately on top of Premium. Faster support has no price at all. A second site is more interesting, and it's the one case below where Enterprise comes out cheaper.&lt;/p&gt;

&lt;p&gt;The prices below are Atlassian's own, read on 30 October 2026, after the 13 October increase. The assumptions sit near the end so you can swap in your own discount. Keep in mind that the Enterprise rows at 200 to 500 users are price list rows nobody offers you in public, and I couldn't confirm that Atlassian or a partner will quote at them. Treat them as the list price a quote would start from.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found: Enterprise is 1.5 times Premium below 801 users too
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Atlassian's Jira Enterprise annual plan in that price list is marked ACTIVE and has 84 user tiers. Eleven of them sit below 801 users, the lowest at $3,000 a year for up to 10 users. The calculator, the Enterprise offer in the page's own structured data (minimum 801 users) and the Cloud Pricing Tables page (Enterprise from 1,000 users) all leave them out.&lt;/li&gt;
&lt;li&gt;Enterprise is 1.51 to 1.52 times Premium at 200, 350, 500 and 801 users. The seat count doesn't change the ratio, so growing doesn't make Enterprise relatively cheaper inside this band.&lt;/li&gt;
&lt;li&gt;Of the priced things most buyers would use, Guard Standard is the biggest. Take it off and the gap per seat per month roughly halves, from $7.29 to $3.67 at 200 users.&lt;/li&gt;
&lt;li&gt;The extra Rovo credits and automation steps are worth $185 to $463 a month to the whole org, and only if you'd otherwise pay overage. They can't close the gap on their own at any size here.&lt;/li&gt;
&lt;li&gt;On our own test site, Rovo Chat logged 19 messages that cost 1,435 credits. Eleven cost 10 credits each and the other eight cost 65 to 310. Premium's 70 credits per user per month is seven short answers, or a fraction of one long multi-step one.&lt;/li&gt;
&lt;li&gt;Inside a tier the per-seat price swings a lot. Enterprise at 201 users is $29.23 per seat per month and $19.58 at 300, because both pay the $70,500 tier.&lt;/li&gt;
&lt;li&gt;With two Jira sites Enterprise can win. 350 people split across two Premium sites, with 50 of them on both, cost $97,100 a year with Guard Standard. Enterprise for the same 350 people is $86,500, so it's $10,600 cheaper.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Jira Premium vs Enterprise pricing at a glance (200, 350, 500 users)
&lt;/h2&gt;

&lt;p&gt;All figures are Atlassian's annual list prices in US dollars, effective 13 October 2026, read from the price data on atlassian.com/software/jira/pricing on 30 October. No partner discount, no tax.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Premium per year&lt;/th&gt;
&lt;th&gt;Enterprise per year&lt;/th&gt;
&lt;th&gt;Premium per seat/month&lt;/th&gt;
&lt;th&gt;Enterprise per seat/month&lt;/th&gt;
&lt;th&gt;Gap per seat/month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200 (tier 101-200)&lt;/td&gt;
&lt;td&gt;$34,500&lt;/td&gt;
&lt;td&gt;$52,000&lt;/td&gt;
&lt;td&gt;$14.38&lt;/td&gt;
&lt;td&gt;$21.67&lt;/td&gt;
&lt;td&gt;$7.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;350 (tier 301-400)&lt;/td&gt;
&lt;td&gt;$57,000&lt;/td&gt;
&lt;td&gt;$86,500&lt;/td&gt;
&lt;td&gt;$13.57&lt;/td&gt;
&lt;td&gt;$20.60&lt;/td&gt;
&lt;td&gt;$7.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500 (tier 401-500)&lt;/td&gt;
&lt;td&gt;$67,000&lt;/td&gt;
&lt;td&gt;$102,000&lt;/td&gt;
&lt;td&gt;$11.17&lt;/td&gt;
&lt;td&gt;$17.00&lt;/td&gt;
&lt;td&gt;$5.83&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;801 (tier 801-1,000)&lt;/td&gt;
&lt;td&gt;$118,000&lt;/td&gt;
&lt;td&gt;$179,000&lt;/td&gt;
&lt;td&gt;$12.28&lt;/td&gt;
&lt;td&gt;$18.62&lt;/td&gt;
&lt;td&gt;$6.35&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 801 row is there as a check. It's the first size where the calculator prints an Enterprise price, and the calculator's $179,000 is exactly the price list row for 801-1,000 users. So the rows below it come from the same list the calculator reads. It just doesn't show them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F47e795b41eeb26294dab9891a890fc81fffc6159-2204x820.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F47e795b41eeb26294dab9891a890fc81fffc6159-2204x820.png" title="The Jira calculator at 350 users, annual billing, 30 October 2026. Notice the Enterprise column: no price, only the 801-user rule and Contact sales." alt="Atlassian's Jira pricing calculator at 350 users on annual billing. Standard shows $32,500 per year and Premium $57,000 per year, both for user tier 301 to 400. The Enterprise column has no price and says Enterprise plan pricing is available when you enter 801 or more users above, with a Contact sales button" width="800" height="298"&gt;&lt;/a&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Ff4c00e8a26dc133b23e9f460021a8c3f3220ddd9-2204x820.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Ff4c00e8a26dc133b23e9f460021a8c3f3220ddd9-2204x820.png" title="Type 801 and Enterprise appears at $179,000 a year, which matches the 801-1,000 row in Atlassian's price list to the dollar." alt="The same Jira pricing calculator at 801 users on annual billing, showing Standard $73,500, Premium $118,000 and Enterprise $179,000 per year, all for user tier 801 to 1,000" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Enterprise has no monthly option. I found no monthly Enterprise plan in the list at all. If you compare against Premium on monthly billing instead, Premium costs $17.13 per seat at 200 users, $14.79 at 350 and $13.42 at 500 (Atlassian's calculator), so the gap narrows by $2.75, $1.22 and $2.25 a seat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch the tier edges
&lt;/h3&gt;

&lt;p&gt;Both plans are sold in user tiers on annual billing, so the price per seat moves a lot inside a band. At 201 users you pay for the 201-300 tier: $19.28 per seat per month on Premium and $29.23 on Enterprise, a gap of almost $10. At 300 users, same tier, it's $12.92 and $19.58. If you're sitting just above a tier line, the comparison looks worse than it will a year from now when you've grown into it. And at 201 users the cheaper Premium is actually the monthly one.&lt;/p&gt;

&lt;p&gt;Premium's annual tiers aren't always cheaper than monthly. Monthly bills per user, so just above a tier line it can win. At 201 to 230 users Premium monthly costs less than Premium annual (at 201 it's $17.11 a seat against $19.28), and the same happens at 101 to 162, 301 to 308, 601 to 638 and 801 to 806. The calculator's "save up to 17%" is a ceiling. Enterprise has no monthly plan, so this only helps the Premium side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jira Enterprise minimum users: why the calculator starts at 801
&lt;/h2&gt;

&lt;p&gt;There's no single page that says "the minimum is 801". The calculator implies it, and the Enterprise offer in the page's structured data has &lt;code&gt;minValue: 801&lt;/code&gt;. Atlassian's Cloud Pricing Tables page goes further and starts its Enterprise table at 1,000 users, with $166,000 before 13 October and $179,000 after. The support article "What is an Enterprise plan?" says Enterprise plans "are aimed at large organizations and have a minimum user tier requirement", with no number, and that purchasing "is handled by our sales team".&lt;/p&gt;

&lt;p&gt;Meanwhile the price list says something else. We pulled the plan rows out of the page HTML with a short script (no login, no API key, the JSON ships to every visitor):&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F3b0f80df5da91b6fb47680a693f9d05f9609927c-2496x1198.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F3b0f80df5da91b6fb47680a693f9d05f9609927c-2496x1198.png" title="The price rows embedded in Atlassian's Jira pricing page on 30 October 2026, printed by our script. Enterprise is priced below 801 too, and the calculator only shows tiers from 801 up." alt="Terminal output listing Atlassian's Jira Premium and Jira Enterprise annual price rows, both marked ACTIVE and dated 13-10-2026. Premium: 101-200 users $34,500, 201-300 $46,500, 301-400 $57,000, 401-500 $67,000, 501-600 $77,500, 601-800 $97,500, 801-1000 $118,000. Enterprise: 101-200 $52,000, 201-300 $70,500, 301-400 $86,500, 401-500 $102,000, 501-600 $117,000, 601-800 $148,000, 801-1000 $179,000" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The full Enterprise plan has 84 user tiers, from $3,000 a year for up to 10 users to $9,805,000 for 90,001 to 100,000, and 11 of them sit below 801. The picture shows the rows from 101 up.&lt;/p&gt;

&lt;p&gt;Customers have been asking about this for years. A 2022 Community question, "Is there a minimum number of users for Enterprise accounts?", has 5,292 views; the asker noticed the same thing, that the calculators "start showing pricing if set to annual billing and at least 801 users". The answer there says there's no minimum, but it isn't from Atlassian staff. A 2025 thread (1,220 views) got the opposite answer and a pointer to sales. And the feature request CLOUD-11064, "Offer Atlassian cloud Enterprise plan subscriptions for user tiers below 1,000", had 349 votes and 279 watchers on 30 October, status Reviewing.&lt;/p&gt;

&lt;p&gt;My reading: the price exists, the public door doesn't. If you're at 200 to 500 users and want Enterprise, you ask Atlassian or a partner for a quote and use the rows above as the list price that quote should start from. Has anyone reading this been quoted Jira Enterprise below 801? I'd like to know what number came back.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F667c3683b141e0c9109e36117b494f29e1716392-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F667c3683b141e0c9109e36117b494f29e1716392-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Atlassian's calculator covers the Enterprise price below 801 users, but the page ships it to your browser anyway. The cloth is mostly decorative." alt="Cartoon of a robot shopkeeper lifting a cloth off a large wooden crate with a sign reading 801 while a small robot customer peers at it through a magnifying glass" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Jira Enterprise cost per user at 200, 350 and 500 users, once you subtract what it includes
&lt;/h2&gt;

&lt;p&gt;To compare fairly, put Enterprise against Premium plus whatever you'd have to buy to get the same priced features. Atlassian publishes prices for three that nearly every buyer would use (sandboxes are a fourth, further down, and only count if you'd buy them).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guard Standard.&lt;/strong&gt; Enterprise includes it "at no extra cost". On Premium, SSO, SCIM and Active Directory sync show as "Requires Atlassian Guard Standard subscription". Guard Standard's annual list price is $8,700 at 200 users, $16,100 at 350 (tier 301-400) and $19,500 at 500. There's a catch in a footnote on Atlassian's support offerings page, written back when Guard was called Access: "We'll only charge for users not using Enterprise products in your Access bill." If your org has Confluence on Premium beside Jira Enterprise, those Confluence-only people are still billed for Guard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rovo credits.&lt;/strong&gt; Jira Premium includes 70 credits per user per month, Enterprise 150. From 3 December, credits beyond the pooled allowance cost $0.01 each, and extra usage is on by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automation steps.&lt;/strong&gt; Premium gives 750 steps per user per month, Enterprise 1,000. Overage from 3 December is $0.50 per 1,000 steps.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Users&lt;/th&gt;
&lt;th&gt;Enterprise minus Premium, per year&lt;/th&gt;
&lt;th&gt;Guard Standard value&lt;/th&gt;
&lt;th&gt;Extra Rovo credits value&lt;/th&gt;
&lt;th&gt;Extra automation value&lt;/th&gt;
&lt;th&gt;Gap left, per year&lt;/th&gt;
&lt;th&gt;Gap left, per seat/month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;$17,500&lt;/td&gt;
&lt;td&gt;$8,700&lt;/td&gt;
&lt;td&gt;$1,920&lt;/td&gt;
&lt;td&gt;$300&lt;/td&gt;
&lt;td&gt;$6,580&lt;/td&gt;
&lt;td&gt;$2.74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;350&lt;/td&gt;
&lt;td&gt;$29,500&lt;/td&gt;
&lt;td&gt;$16,100&lt;/td&gt;
&lt;td&gt;$3,360&lt;/td&gt;
&lt;td&gt;$525&lt;/td&gt;
&lt;td&gt;$9,515&lt;/td&gt;
&lt;td&gt;$2.27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;$35,000&lt;/td&gt;
&lt;td&gt;$19,500&lt;/td&gt;
&lt;td&gt;$4,800&lt;/td&gt;
&lt;td&gt;$750&lt;/td&gt;
&lt;td&gt;$9,950&lt;/td&gt;
&lt;td&gt;$1.66&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;How the two allowance columns are worked out: (150 − 70) credits × users × 12 months × $0.01, and (1,000 − 750) steps × users × 12 × $0.0005. Both are ceilings. They're only money you save if your org would otherwise blow past the Premium pool month after month, and unused credits and steps don't roll over.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F016d6d946b23d316b03514b8acb26dfc4c914a10-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F016d6d946b23d316b03514b8acb26dfc4c914a10-1600x896.png" title="Red is what Enterprise costs over Premium per seat per month on annual list prices. Blue is what's left after you subtract Guard Standard and the full value of the bigger allowances. The blue bar stays above zero." alt="Bar chart of the extra cost of Jira Enterprise over Premium per seat per month. At 200 users the list gap is $7.29 and the gap left after Guard Standard and the bigger allowances is $2.74. At 350 users, $7.02 and $2.27. At 500 users, $5.83 and $1.66" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So even if you need Guard and you'd use up the extra credits and steps, Enterprise still costs $6,580 to $9,950 a year more at these sizes. You're paying that for the rows with no price tag.&lt;/p&gt;

&lt;p&gt;Which number goes in the budget? If you'd buy Guard Standard anyway, use the Premium plus Guard comparison: $8,800 a year more for Enterprise at 200 users ($3.67 a seat a month), $13,400 at 350 and $15,500 at 500. If you wouldn't buy Guard, the full list gap applies, $17,500 to $35,000 a year. The allowance columns only come off if you already expect overage.&lt;/p&gt;

&lt;p&gt;I priced only Jira. If Confluence sits in the same organisation, Enterprise is a separate decision per app, and the Guard footnote below decides who still gets a Guard bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changes on 3 December: Rovo credits and automation allowances, Premium vs Enterprise
&lt;/h2&gt;

&lt;p&gt;The allowances are pooled across the org. At 200 users, Premium gives the pool 14,000 Rovo credits a month and Enterprise 30,000. At 500 users it's 35,000 against 75,000. A company with Jira and Confluence on the same plan gets both apps' allowances in one pool (we covered how the pool works in our &lt;a href="https://leanzero.net/blog/atlassian-usage-based-pricing-budget-2027?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;2027 usage-based pricing budget for 50, 200 and 500 seats&lt;/a&gt;, so I won't repeat it here).&lt;/p&gt;

&lt;p&gt;Is 70 credits per person enough? It depends on what people do with Rovo. On our test site, Rovo Chat logged 19 messages between 8 September and 20 October, 1,435 credits in all. Eleven cost 10 credits each and the other eight cost 65 to 310. A Rovo agent called from an automation rule costs 10 credits a call, and a single "Think deeper" answer cost anywhere from 10 to 182 credits when we &lt;a href="https://leanzero.net/blog/rovo-credits-cost-think-deeper?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;re-measured it after getting it wrong the first time&lt;/a&gt;. So Premium's allowance is seven short answers per person per month, or a fraction of one long multi-step one, and Enterprise's is fifteen short ones. In a pooled allowance light users cover heavy ones, and how many light users you have is the question. If you've rolled Rovo out to everyone and people use it daily, you'll probably run past the Premium pool. Past that point both plans charge $0.01 a credit. So the most Enterprise can save you is the 80 extra credits per user per month.&lt;/p&gt;

&lt;p&gt;I think automation is smaller money than it looks. At 200 users the Enterprise pool has 50,000 more steps a month than Premium. A scheduled rule that runs every five minutes and matches nothing still uses one step per run, which is 8,640 steps a month (we &lt;a href="https://leanzero.net/tutorials/jira-automation-usage-billing-steps-per-rule?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;counted steps rule by rule&lt;/a&gt;). So the Enterprise bonus covers about six forgotten rules like that, and if you paid for them as overage instead it would be $25 a month.&lt;/p&gt;

&lt;p&gt;A small oddity. The FAQ on Atlassian's own Jira pricing page still says Enterprise comes with "unlimited automation" and "the inclusion of Atlassian Access". The compare table on the same page and the automation documentation both say 1,000 steps per user per month, and Access has been called Guard for a while now. I'd budget on 1,000.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jira Enterprise features Premium does not have
&lt;/h2&gt;

&lt;p&gt;These are the rows that differ between the two columns on Atlassian's compare table, plus what the support and SLA pages add. Data residency, IP allowlisting, release tracks and admin insights are on both plans, so they don't belong in this decision even though some partner pages list them under Enterprise. Sandboxes are on both too, one free against five.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Premium&lt;/th&gt;
&lt;th&gt;Enterprise&lt;/th&gt;
&lt;th&gt;Has a list price?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sites&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Up to 150&lt;/td&gt;
&lt;td&gt;No, but a second Premium site is a second subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Critical-issue response&lt;/td&gt;
&lt;td&gt;1 hour, cloud support team, no phone&lt;/td&gt;
&lt;td&gt;30 minutes, dedicated senior team, phone number&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime SLA&lt;/td&gt;
&lt;td&gt;99.9%&lt;/td&gt;
&lt;td&gt;99.95%&lt;/td&gt;
&lt;td&gt;No (service credits only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free sandboxes per app&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Extra ones $10,000 each, per app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atlassian Data Lake, data catalog and lineage&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multiple identity providers&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit logs of user activity&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom Jira templates&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Centralized user subscriptions, user counts, app requests&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guard Standard&lt;/td&gt;
&lt;td&gt;Bought separately&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Yes (in the table above)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Multiple sites is where Enterprise can pay for itself
&lt;/h3&gt;

&lt;p&gt;On Premium, each Jira site is its own subscription. Two Premium sites of 100 users each cost 2 × $19,500 = $39,000 a year, against $34,500 for one site of 200 (my reading of how separate subscriptions are tiered, using the 51-100 row of the same price list). As I read it, anyone with access to both sites counts on both bills. Enterprise covers "up to 150 instances of an Atlassian app without paying extra for those instances", so a company that has to keep a separate site per region, per client or per regulated unit stops paying per site. A partner in a 2022 Community thread on when to move to Enterprise (4,463 views) put it as predictable pricing: "you know what it's going to cost no matter how many new sites the team spins up". Of all the arguments here, it's the one I'd take seriously at 200 to 500 users. The worked case below shows why.&lt;/p&gt;

&lt;h3&gt;
  
  
  A worked case: 350 people on two Jira sites
&lt;/h3&gt;

&lt;p&gt;Say you run one site for the product teams (200 people) and a separate one for a regulated unit (150 people), and you want SSO. On Premium that's two subscriptions. 200 users is the 101-200 tier at $34,500, and 150 users is the same tier, another $34,500. Guard Standard bills people in your organisation, so 350 users is $16,100. Total: $85,100 a year.&lt;/p&gt;

&lt;p&gt;Enterprise for 350 users is $86,500 with Guard included and one subscription covering both sites. The difference is $1,400 a year, about 33 cents per seat per month, and you also get the 30-minute support, the bigger allowances and the rest of the table.&lt;/p&gt;

&lt;p&gt;Now put 50 people on both sites, which happens a lot (managers, admins, the security team). On Premium the first site becomes 250 users, the 201-300 tier at $46,500, and the second stays at $34,500. With Guard that's $97,100. Enterprise still counts 350 people. Atlassian's Enterprise doc says "you only pay once for each user, no matter how many instances of the Atlassian app they have access to", so it stays at $86,500 and comes out $10,600 a year cheaper. Atlassian doesn't spell out the Premium side. My reading is that two Premium sites are two subscriptions and a person on both is billed twice, and it's the first thing I'd ask a partner to confirm in writing.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 99.95% SLA adds a 5% credit, about $217 for a bad month at 200 users
&lt;/h3&gt;

&lt;p&gt;Enterprise adds a service credit of 5% of the monthly fee when uptime falls between 99.9% and 99.95%. At 200 users that's 5% of $52,000 ÷ 12, about $217, for a 30-day month in which Jira was down between 22 and 43 minutes. Below 99.9% both plans pay the same 10%, 25% and 50% credits. I wouldn't buy Enterprise for the SLA credit. During an outage you care about the 30-minute response, and the credit is something you have to claim within 15 days of the month's end.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fbcbf1658b6f71dec9ee2eb22fb1072a6c5d69297-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fbcbf1658b6f71dec9ee2eb22fb1072a6c5d69297-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Guard Standard plus the extra credits and steps still don't balance the Enterprise price at 200 to 500 users. The referee has stopped writing." alt="Cartoon of two robots on a seesaw in a playground. On the low end sits a heavy money sack next to a robot; on the high end a robot is piling gift boxes, a shield and a battery pack, while a referee robot with a clipboard watches" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sandboxes, only if you'd buy them anyway
&lt;/h3&gt;

&lt;p&gt;Enterprise includes five free sandboxes per app, Premium one. Extra sandboxes cost "$10,000 per sandbox, per app" and need annual billing. If your change process really needs four more Jira sandboxes, that's $40,000 of value and Enterprise comes out cheaper. Atlassian's page doesn't say whether the $10,000 is per year or one-off, and I'd ask before putting it in a business case.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask before you sign an Enterprise quote
&lt;/h2&gt;

&lt;p&gt;If you do go to sales, these are the questions I'd want answered in writing next to the quote, because each one moves the comparison above.&lt;/p&gt;

&lt;p&gt;Start with how users are counted. Ask whether someone on two of your sites counts once or twice. The whole two-site case hangs on it.&lt;/p&gt;

&lt;p&gt;Then ask which price row the quote starts from. Bring the catalogue figures: $52,000 at 200 users, $86,500 at 350, $102,000 at 500. If the quote is higher, ask why.&lt;/p&gt;

&lt;p&gt;Ask about Guard for people outside Enterprise. Atlassian's footnote says it only charges for "users not using Enterprise products" (the footnote still calls Guard by its old name, Access). If Confluence stays on Premium, those people are still on a Guard invoice.&lt;/p&gt;

&lt;p&gt;Ask about your Marketplace apps. Enterprise "requires a switch to annual subscriptions for the Atlassian apps and all Marketplace apps", and their cost is not in the Enterprise price. A monthly app bill that turns annual is a cash-flow change even if the total stays the same.&lt;/p&gt;

&lt;p&gt;Ask what happens to the Premium months you've already paid. Atlassian says it will credit the unused term toward Enterprise. Get the credit amount on the quote.&lt;/p&gt;

&lt;p&gt;And ask whether the $10,000 extra sandbox is a year or forever. I couldn't find it written down.&lt;/p&gt;

&lt;p&gt;None of this needs a partner, but a partner who has done a few Enterprise quotes will know the answers without asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  These are list prices: what would change them
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Atlassian list prices in USD from the price data on atlassian.com/software/jira/pricing (Jira) and atlassian.com/software/guard/pricing (Guard), effective 13 October 2026, read 30 October 2026. Plan strings: "Jira, Premium, Annual (USD, Default, 13-10-2026)" and "Jira, Enterprise, Annual (USD, Default, 13-10-2026)".&lt;/li&gt;
&lt;li&gt;Annual billing for both plans, since Enterprise is annual only. No partner or volume discount. Enterprise is sold through sales, so a real quote may differ, and nothing I found confirms a quote at the sub-801 rows.&lt;/li&gt;
&lt;li&gt;Guard Standard counted for the same number of users as Jira. If your org has more users than Jira users, Guard costs more on Premium.&lt;/li&gt;
&lt;li&gt;Rovo and automation allowance values assume your org would pay overage for all the extra credits and steps. If you stay inside the Premium pool, those columns are $0 and the gap is bigger. Rates from 3 December 2026: $0.01 per Rovo credit, $0.50 per 1,000 automation steps.&lt;/li&gt;
&lt;li&gt;Moving from Premium mid-term: Atlassian says it "will credit the value of the unused maintenance term toward the Cloud Enterprise purchase". Enterprise also needs all your Marketplace apps on annual billing, and their costs aren't included. A quote dated before 13 October is honoured at the old prices until it expires. Premium went from $32,000 to $34,500 at 200 users (+7.8%), and Enterprise from $166,000 to $179,000 at 1,000 (+7.8%).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Should a 200-500 user company move from Premium to Enterprise?
&lt;/h2&gt;

&lt;p&gt;At 200 to 500 users, stay on Premium unless one of these is true. You need more than one Jira site, or soon will. You need a phone line and a 30-minute response when Jira is down. Your security team insists on multiple identity providers, Data Lake or user activity audit logs. If you need none of that and want SSO, I'd add Guard Standard to Premium. You'll pay $3.67 a seat a month less than Enterprise at 200 users and $2.58 less at 500.&lt;/p&gt;

&lt;p&gt;If one of them does apply, ask for a quote and bring the price list rows with you. $52,000 at 200 users, $86,500 at 350 and $102,000 at 500 are what Atlassian's own catalogue says, and a quote above them is worth questioning.&lt;/p&gt;

&lt;p&gt;Whatever you pick, set an extra usage limit for Rovo credits in Atlassian Administration before 3 December, because it's on by default and neither plan caps it for you.&lt;/p&gt;

&lt;p&gt;Did you get a Jira Enterprise quote under 801 users, or get told no? And if you moved from Premium, which of the unpriced rows actually decided it for you? Tell me in the comments. I'm especially curious whether anyone went to Enterprise for the sites alone.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/blog/jira-premium-vs-enterprise-pricing-200-500-seats?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>jira</category>
      <category>atlassian</category>
      <category>pricing</category>
      <category>saas</category>
    </item>
    <item>
      <title>Teamwork Collection Pricing vs Jira, Confluence and Loom</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Fri, 30 Oct 2026 11:58:42 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/teamwork-collection-pricing-vs-jira-confluence-and-loom-2d2h</link>
      <guid>https://dev.to/mihai_leanzero/teamwork-collection-pricing-vs-jira-confluence-and-loom-2d2h</guid>
      <description>&lt;p&gt;Someone on the Atlassian Community asked a question in a thread that's now been read 2,299 times. Their company wanted Teamwork Collection. They also had freelancers who "should have access to Jira but should not have access to Confluence or Loom". So could they just book a few extra Jira licences for them? After the answers came in, they summed it up themselves: "Using the Teamwork Collection will result in paying for the whole collection, even if they only have access to one of the apps."&lt;/p&gt;

&lt;p&gt;It's a fair question for anyone weighing the collection, and the calculator doesn't answer it. On Atlassian's list prices, 200 people on Standard pay $12.29 a seat per month for the collection on annual billing and $25.83 for the three apps bought separately, as long as all of them use all three. To see what happens when they don't, we took Atlassian's own list prices as they were on 30 October 2026, after the 13 October increase. Then we worked out the cost per seat per month both ways for 50, 200 and 500 users. We did it on Standard and on Premium, with and without Loom. Then we added the usage meters that start being billed on 3 December (Rovo credits and automation steps).&lt;/p&gt;

&lt;p&gt;If everyone in your company really uses all three apps, I think the collection is an easy yes: about half the cost per seat on Standard, and 29% to 50% less on Premium. Take Loom out and it's close to a tie on Standard. On Premium above 200 seats the collection costs more. If you mostly want Jira, it's an expensive way to get it.&lt;/p&gt;

&lt;p&gt;This is for whoever has to put a number in next year's budget: an IT manager, an Atlassian product owner, someone in finance. The prices come from Atlassian's pages, read on 30 October, and the assumptions sit at the end so you can swap in your own. Our &lt;a href="https://leanzero.net/blog/atlassian-usage-based-pricing-budget-2027?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;2027 usage-based pricing budget for 50, 200 and 500 seats&lt;/a&gt; compared the collection with Jira plus Confluence. It left out Loom, users who only need one app, and the billing rules that decide whether the collection is cheap for you in particular.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found: Loom decides whether Teamwork Collection is cheaper
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;With all three apps for everyone, the collection is 49% to 57% cheaper per seat on Standard and 29% to 50% cheaper on Premium, across 50, 200 and 500 users.&lt;/li&gt;
&lt;li&gt;Loom is what makes the collection cheap. Without it, Standard saves $500 to $1,000 a year at our three sizes, and Premium costs $24,000 a year more at 500 seats.&lt;/li&gt;
&lt;li&gt;The collection charges a full seat for anyone who can open any one of its apps. Jira-only people can't be carved out, editions can't be mixed, and an app can't be swapped out.&lt;/li&gt;
&lt;li&gt;Partial adoption saves less than you'd think on annual billing, because the apps are priced in user tiers.&lt;/li&gt;
&lt;li&gt;The collection's Rovo credit and automation step allowances are five times what Jira and Confluence give together. Whether that's worth money depends on usage nobody can measure for you yet.&lt;/li&gt;
&lt;li&gt;Atlassian's own pages disagree on two details (whether standalone Loom adds Rovo credits, and whether the Standard collection includes Rovo agents). We show both.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Teamwork Collection pricing vs Jira, Confluence and Loom bought separately: about half on Standard
&lt;/h2&gt;

&lt;p&gt;Teamwork Collection is Atlassian's bundle of Jira, Confluence and Loom. It also brings Rovo, Atlassian's AI assistant, and the platform apps (Home, Goals, Teams, Studio, Search, Chat, Insights and Admin, as the pricing page lists them). The &lt;a href="https://www.atlassian.com/collections/teamwork/pricing" rel="noopener noreferrer"&gt;Teamwork Collection pricing page&lt;/a&gt; says you'll "pay a single price for the entire collection". You won't pay separately for the products inside it.&lt;/p&gt;

&lt;p&gt;To compare it with buying the apps one by one, you have to decide which Loom plan the collection stands in for, and Atlassian doesn't say. We matched Standard with Loom Business and Premium with Loom Business + AI. Our reason is the &lt;a href="https://support.atlassian.com/rovo/docs/rovo-usage-limits/" rel="noopener noreferrer"&gt;Rovo credits table&lt;/a&gt;, which pairs "Loom Business plan" with Standard's 25 credits and "Loom Business + AI plan" with Premium's 70. The collection page also mentions Loom AI only from Premium up. If your company would buy plain Loom Business even on Premium, the separate column gets cheaper.&lt;/p&gt;

&lt;p&gt;On annual billing, licences only, with all three apps for each user, it looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Seats&lt;/th&gt;
&lt;th&gt;Standard: separate&lt;/th&gt;
&lt;th&gt;Standard: collection&lt;/th&gt;
&lt;th&gt;Premium: separate&lt;/th&gt;
&lt;th&gt;Premium: collection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;$17,450 a year ($29.08/seat/mo)&lt;/td&gt;
&lt;td&gt;$7,500 ($12.50)&lt;/td&gt;
&lt;td&gt;$28,900 ($48.17)&lt;/td&gt;
&lt;td&gt;$14,500 ($24.17)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;$62,000 ($25.83)&lt;/td&gt;
&lt;td&gt;$29,500 ($12.29)&lt;/td&gt;
&lt;td&gt;$101,000 ($42.08)&lt;/td&gt;
&lt;td&gt;$58,000 ($24.17)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;$126,500 ($21.08)&lt;/td&gt;
&lt;td&gt;$64,000 ($10.67)&lt;/td&gt;
&lt;td&gt;$199,500 ($33.25)&lt;/td&gt;
&lt;td&gt;$141,000 ($23.50)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The separate column adds up three list prices. At 200 users on Standard that's Jira at $18,000, Confluence at $12,500 and Loom Business at $31,500. Loom is the biggest line, $31,500 of the $62,000.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fbb749404bb0673f382d8c507eef1b39ff3a0c4a2-1540x792.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fbb749404bb0673f382d8c507eef1b39ff3a0c4a2-1540x792.png" title="Cost per seat per month on Atlassian's annual list prices of 30 October 2026, seats only. Notice the right panel: without Loom the bars are nearly level, and at 500 Premium seats the orange collection bar is the longer one." alt="Bar chart of cost per seat per month on annual list prices. Left panel, everyone uses all three apps: separate vs collection is $29.08 vs $12.50 (Standard, 50), $25.83 vs $12.29 (Standard, 200), $21.08 vs $10.67 (Standard, 500), $48.17 vs $24.17 (Premium, 50), $42.08 vs $24.17 (Premium, 200), $33.25 vs $23.50 (Premium, 500). Right panel, nobody needs Loom: $14.08 vs $12.50, $12.71 vs $12.29, $10.75 vs $10.67, $28.17 vs $24.17, $24.58 vs $24.17, $19.50 vs $23.50" width="799" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Atlassian Teamwork Collection cost per user on monthly Standard billing drops from $15.00 to $12.77
&lt;/h2&gt;

&lt;p&gt;Atlassian's calculator shows monthly prices by default. At 200 users and monthly billing it shows $14.70 per user for the Standard collection and $29 for Premium.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F89d60ce0a2523ae0189e658eec37f10adce9edaa-1440x1000.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F89d60ce0a2523ae0189e658eec37f10adce9edaa-1440x1000.png" title="The collection calculator on 30 October 2026 at 200 users, monthly. Standard $14.70, Premium $29, and Enterprise only appears on annual billing above 800 users." alt="Atlassian's Teamwork Collection pricing calculator with team size 200 users and monthly billing selected, showing Free at $0, Standard at $14.70 per user per month, Premium at $29 per user per month marked Recommended, and Enterprise asking you to switch to annual billing" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Monthly billing on Atlassian Cloud is graduated: the first 100 users pay one rate, the next band of users a lower one, and so on. Annual billing is a flat price for the user tier you fall into. So the same 200 people cost $14.70 a seat a month on monthly billing and $12.29 on annual. Monthly, side by side:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Seats, monthly billing&lt;/th&gt;
&lt;th&gt;Standard: separate&lt;/th&gt;
&lt;th&gt;Standard: collection&lt;/th&gt;
&lt;th&gt;Premium: separate&lt;/th&gt;
&lt;th&gt;Premium: collection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;$34.85&lt;/td&gt;
&lt;td&gt;$15.00&lt;/td&gt;
&lt;td&gt;$57.70&lt;/td&gt;
&lt;td&gt;$29.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;$31.00&lt;/td&gt;
&lt;td&gt;$14.70&lt;/td&gt;
&lt;td&gt;$50.27&lt;/td&gt;
&lt;td&gt;$29.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;$25.27&lt;/td&gt;
&lt;td&gt;$12.77&lt;/td&gt;
&lt;td&gt;$39.84&lt;/td&gt;
&lt;td&gt;$28.28&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We checked the price data we used against what Atlassian's pages actually display. At 200 users the collection calculator shows $14.70 and $29 and the Jira one shows $8.95 and $17.13. Loom at 100 users shows $18 for Business and $24 for Business + AI. Our numbers match all of them, and they also match the Jira and Confluence rows in Atlassian's &lt;a href="https://www.atlassian.com/licensing/future-pricing/cloud/list/pricing-tables" rel="noopener noreferrer"&gt;cloud pricing tables&lt;/a&gt;. That page has no Teamwork Collection table, so use the calculator to check collection prices.&lt;/p&gt;

&lt;p&gt;Enterprise is left out on purpose. The pricing page says "Enterprise plan pricing is available when you enter 801 or more users", annual only, so none of our three sizes can buy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teamwork Collection Premium pricing beats Jira and Confluence Premium only up to 200 seats without Loom
&lt;/h2&gt;

&lt;p&gt;Premium is where the answer depends most on Loom. With Loom Business + AI for everyone, the collection saves $14,400 a year at 50 seats, $43,000 at 200 and $58,500 at 500. Without Loom, the savings almost disappear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Premium, no Loom, annual&lt;/th&gt;
&lt;th&gt;Jira + Confluence Premium&lt;/th&gt;
&lt;th&gt;Teamwork Collection Premium&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50 seats&lt;/td&gt;
&lt;td&gt;$16,900&lt;/td&gt;
&lt;td&gt;$14,500&lt;/td&gt;
&lt;td&gt;collection $2,400 cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 seats&lt;/td&gt;
&lt;td&gt;$59,000&lt;/td&gt;
&lt;td&gt;$58,000&lt;/td&gt;
&lt;td&gt;collection $1,000 cheaper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500 seats&lt;/td&gt;
&lt;td&gt;$117,000&lt;/td&gt;
&lt;td&gt;$141,000&lt;/td&gt;
&lt;td&gt;collection $24,000 dearer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We ran every seat count from 11 to 800. On annual Premium with no Loom, the collection becomes the more expensive option from 201 seats onward. On monthly billing it happens from 214. The 500-seat figures match our &lt;a href="https://leanzero.net/blog/atlassian-usage-based-pricing-budget-2027?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;usage-based pricing budget&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two things can still tip a no-Loom Premium buyer towards the collection. The first is Atlassian Guard, Atlassian's paid security layer (single sign-on enforcement, user provisioning, audit features). The &lt;a href="https://support.atlassian.com/subscriptions-and-billing/docs/how-billing-works-for-collections/" rel="noopener noreferrer"&gt;billing doc for collections&lt;/a&gt; says Premium and Enterprise collections "have free access to Atlassian Guard Standard", and Guard Standard's own annual list price is $2,200 at 50 users, $8,700 at 200 and $19,500 at 500. If you'd buy Guard Standard anyway, the 500-seat gap shrinks from $24,000 to $4,500.&lt;/p&gt;

&lt;p&gt;The second is Rovo credits, the units Rovo spends per answer or agent action. The Premium collection gives 700 credits per user per month, against 70 each for Jira Premium and Confluence Premium. At 500 seats, with no Loom, the collection comes out cheaper once the average person uses more than 540 credits a month, or more than 215 if you count Guard. I'd call 540 a lot of Rovo. On our own site a single answer in Rovo's &lt;a href="https://leanzero.net/blog/rovo-credits-cost-think-deeper?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Think deeper mode used anywhere from 10 to 182 credits&lt;/a&gt;. A few heavy users can get to 540. A whole company of 500 averaging it is a big assumption to put in a budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you buy additional Jira licenses on a Teamwork Collection site?
&lt;/h2&gt;

&lt;p&gt;Back to the freelancers. Inside a collection, no. The billing doc says "each user who has access to least one of the included apps will be charged as a unique billable user" (the typo is Atlassian's). It counts that user once "even if the user has access to multiple apps". So a contractor who only opens Jira still counts as a full collection user.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fd1b406d36121999326441e81879bdf3294d67248-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fd1b406d36121999326441e81879bdf3294d67248-1600x896.png" title="Illustration, generated locally with ERNIE-Image. A Jira-only user inside Teamwork Collection is billed as a full collection user. It only came in for the one box." alt="Cartoon of a small blue robot holding one cardboard box while a robot cashier behind a counter with a basket of three gift boxes hands a long receipt to another robot" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How much that costs depends on how you're billed. Monthly billing is graduated, so each extra person is charged at the rate of their band. Between 101 and 250 users a Standard collection user costs $14.40 a month. If the company bought the three apps separately instead, those same people would be Jira users 171 to 200, at $8.20 each. Take a 200-person company where 30 people are Jira-only contractors. As collection users they cost $432 a month. As Jira users they'd cost $246. That's about $2,232 a year more, for apps they never open.&lt;/p&gt;

&lt;p&gt;Annual billing works in tiers, which changes the answer. A Standard collection costs $29,500 a year for anyone from 101 to 200 users, so 170 staff plus 30 contractors pay exactly what 170 would. User 201 moves you to the next tier at $42,000. So on annual billing your contractors are either free or very expensive. It depends on where they put you in the tier.&lt;/p&gt;

&lt;p&gt;Atlassian's own &lt;a href="https://www.atlassian.com/licensing/teamwork-collection" rel="noopener noreferrer"&gt;Teamwork Collection licensing FAQ&lt;/a&gt; is fairly upfront about who should skip it. It says buying the apps separately "may be the right choice" in a few cases. One is if you "want to enable significantly more users for one app than another". Another is if you "want different app editions for Jira, Confluence, and Loom", and the last is if you "don't anticipate using AI to improve teamwork outcomes". That middle condition means editions can't be mixed. You can't have Jira Premium for the engineers and Confluence Standard for everyone else inside one collection.&lt;/p&gt;

&lt;p&gt;A few more rules from the same FAQ that I'd want to know before signing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can't swap one of the apps for something else: "The apps available in a Teamwork Collection aren't customizable."&lt;/li&gt;
&lt;li&gt;Removing an app "takes effect immediately, and no credit or refund is provided for unused time".&lt;/li&gt;
&lt;li&gt;Cancelling the collection "cancels all underlying apps". Opting out of a collection trial "keeps individual apps active", which is gentler.&lt;/li&gt;
&lt;li&gt;If you already pay for Jira or Confluence, you're "eligible for a credit for any unused time". Mixed billing or annual quotes still mean contacting Atlassian.&lt;/li&gt;
&lt;li&gt;Marketplace apps you move into a collection "may impact their pricing". The doc doesn't say in which direction or by how much, so ask each vendor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://www.praecipio.com/resources/articles/everything-to-know-about-atlassian-teamwork-collection" rel="noopener noreferrer"&gt;Praecipio's guide to the collection&lt;/a&gt;, from an Atlassian partner, says all users on the site become billable once the collection is switched on. Atlassian's billing doc says the trigger is having access to at least one of the included apps. I'd go with Atlassian's wording. Before switching, check which groups get product access. Each person in them becomes a billable collection user. Our &lt;a href="https://leanzero.net/blog/find-inactive-users-jira-cost-slasher?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;guide to finding inactive Jira and Confluence users&lt;/a&gt; shows how to find the ones who left.&lt;/p&gt;

&lt;h2&gt;
  
  
  When buying separately is cheaper: the share of users who never open Confluence or Loom
&lt;/h2&gt;

&lt;p&gt;Most companies aren't "everyone uses everything". So we modelled a few mixes.&lt;/p&gt;

&lt;p&gt;Say nobody needs Loom and everyone uses Jira. How many of them also need Confluence before the collection becomes the cheaper option?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;No Loom, everyone on Jira&lt;/th&gt;
&lt;th&gt;50 seats&lt;/th&gt;
&lt;th&gt;200 seats&lt;/th&gt;
&lt;th&gt;500 seats&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Standard, annual&lt;/td&gt;
&lt;td&gt;51% need Confluence&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;td&gt;81%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard, monthly&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;97%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium, annual&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;td&gt;51%&lt;/td&gt;
&lt;td&gt;never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium, monthly&lt;/td&gt;
&lt;td&gt;67%&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;td&gt;never&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On annual Standard the collection wins once about half your Jira users also live in Confluence. On monthly billing you need almost everyone. And on Premium at 500 seats, licences alone never get you there.&lt;/p&gt;

&lt;p&gt;Loom needs less adoption than you'd expect, because of how annual tiers work. If everyone has Jira and Confluence on Premium, the collection is cheaper at 500 seats once at least 19% of people would otherwise need Loom Business + AI. At 800 seats it's 26%. On Standard the collection was already cheaper with 0% Loom at our three sizes.&lt;/p&gt;

&lt;p&gt;Loom's annual plans are sold "for the tier that most closely matches your user count", per the &lt;a href="https://www.atlassian.com/software/loom/pricing" rel="noopener noreferrer"&gt;Loom pricing page&lt;/a&gt;. In the price data, 120 users and 200 users both pay $31,500 a year for Loom Business. So "we'll only give Loom to 60% of people" saves nothing at 200 seats on annual billing. At 500 seats it does help, because 300 users drop to a lower tier ($43,000 against $62,000).&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F54f0a8de99862bdc23ffb63bf387db4397d9860e-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F54f0a8de99862bdc23ffb63bf387db4397d9860e-1600x896.png" title="Illustration, generated locally with ERNIE-Image. On Loom's annual tiers, 120 users pay the same $31,500 a year as 200. Both buses pay the same fare." alt="Cartoon of a robot counting a tall stack of coins next to a bus carrying a handful of robots, with a second bus full of robots waiting behind it" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's a more realistic mix, with everyone on Confluence, 70% on Jira and 60% on Loom, annual:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;70% Jira, 100% Confluence, 60% Loom&lt;/th&gt;
&lt;th&gt;Standard separate&lt;/th&gt;
&lt;th&gt;Standard collection&lt;/th&gt;
&lt;th&gt;Premium separate&lt;/th&gt;
&lt;th&gt;Premium collection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50 seats&lt;/td&gt;
&lt;td&gt;$13,850&lt;/td&gt;
&lt;td&gt;$7,500&lt;/td&gt;
&lt;td&gt;$24,100&lt;/td&gt;
&lt;td&gt;$14,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 seats&lt;/td&gt;
&lt;td&gt;$62,000&lt;/td&gt;
&lt;td&gt;$29,500&lt;/td&gt;
&lt;td&gt;$101,000&lt;/td&gt;
&lt;td&gt;$58,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500 seats&lt;/td&gt;
&lt;td&gt;$101,000&lt;/td&gt;
&lt;td&gt;$64,000&lt;/td&gt;
&lt;td&gt;$164,500&lt;/td&gt;
&lt;td&gt;$141,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 200-seat row is the same as in the all-three table, and that isn't a copy error. On annual billing, 140 Jira users and 120 Loom users both fall into the 101-200 tier, so they pay the 200-user price.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 3 December 2026 usage meters add to each option
&lt;/h2&gt;

&lt;p&gt;From 3 December, usage above your allowance gets billed. The &lt;a href="https://support.atlassian.com/rovo/docs/rovo-usage-limits/" rel="noopener noreferrer"&gt;Rovo credits doc&lt;/a&gt; says "On December 3, 2026, extra usage billing takes effect for Rovo credits", and the &lt;a href="https://support.atlassian.com/cloud-automation/docs/how-is-my-usage-calculated/" rel="noopener noreferrer"&gt;automation usage doc&lt;/a&gt; says the same for steps. The Rovo doc adds that extra usage "is enabled by default", and admins can set a spending limit or switch it off. It costs $0.01 per credit and $0.50 per 1,000 steps.&lt;/p&gt;

&lt;p&gt;In both cases the allowances go into one pool for the whole organisation ("All of your organization's Rovo credit allowances are combined into a single shared pool"). So compare the size of the pool:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Per user per month&lt;/th&gt;
&lt;th&gt;Jira&lt;/th&gt;
&lt;th&gt;Confluence&lt;/th&gt;
&lt;th&gt;Loom&lt;/th&gt;
&lt;th&gt;Teamwork Collection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rovo credits, Standard&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;25 (support doc) or none (licensing page)&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rovo credits, Premium&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;70 (support doc) or none (licensing page)&lt;/td&gt;
&lt;td&gt;700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automation steps, Standard&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;not listed&lt;/td&gt;
&lt;td&gt;2,500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automation steps, Premium&lt;/td&gt;
&lt;td&gt;750&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;not listed&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Loom cells are one of the places Atlassian disagrees with itself. The support doc has a Loom row. The &lt;a href="https://www.atlassian.com/licensing/rovo" rel="noopener noreferrer"&gt;Rovo licensing page&lt;/a&gt; has no Loom row. We used the support doc, which is the friendlier reading for the separate option. The other disagreement is about agents. The billing doc lists "Rovo agents (with Premium and Enterprise)" for the collection, while the pricing page's Standard column says "Rovo Search, Chat, and Agents". Ask your Atlassian contact which one is right for your contract.&lt;/p&gt;

&lt;p&gt;I don't know how much usage you should plan for, and probably nobody does yet. Our own measurements swung too much to price a "typical user" (that's why we corrected &lt;a href="https://leanzero.net/blog/rovo-credits-cost-think-deeper?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;our Think deeper numbers&lt;/a&gt;), so we used round levels instead. At 300 credits and 300 steps per person per month on Standard, the annual totals become:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;300 credits, 300 steps per person&lt;/th&gt;
&lt;th&gt;Separate (all three apps)&lt;/th&gt;
&lt;th&gt;Collection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50 seats&lt;/td&gt;
&lt;td&gt;$18,800 ($31.33/seat/mo)&lt;/td&gt;
&lt;td&gt;$7,800 ($13.00)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;200 seats&lt;/td&gt;
&lt;td&gt;$67,400 ($28.08)&lt;/td&gt;
&lt;td&gt;$30,700 ($12.79)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500 seats&lt;/td&gt;
&lt;td&gt;$140,000 ($23.33)&lt;/td&gt;
&lt;td&gt;$67,000 ($11.17)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On Premium at 700 credits and 1,000 steps, 200 seats cost $112,760 separately against $58,000 for the collection. With everyone on Loom, the bigger pool only widens the gap. Where it matters is the no-Loom Premium case above, at 540 credits a person.&lt;/p&gt;

&lt;p&gt;If you want to see your own numbers before you decide, Atlassian Administration already shows the meters. This is our test organisation. We don't run Teamwork Collection there (we have Service Collection Premium for four users). Even so, the Rovo credits row is already listed as "Billable Usage", and the Atlassian Guard Standard row shows 0 users and USD 0.00.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F6d627b98862001781d697f2e40866637d2c53e75-1110x397.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F6d627b98862001781d697f2e40866637d2c53e75-1110x397.png" title="Our test org's Billing and subscriptions page on 30 October 2026. Rovo credits already sits there as Billable Usage, a month before extra usage is billed." alt="Billing and subscriptions table in Atlassian Administration for our test organisation, with rows for Rovo credits on Billable Usage at USD 0.00, Service Collection Free with 3 users, Service Collection Premium with 4 of 4 users at USD 246.40 monthly, Atlassian Guard Standard with 0 users at USD 0.00, Atlassian Guard Premium Add-On at USD 44.80, Teams Free and Assets objects on Included Usage" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have walkthroughs for &lt;a href="https://leanzero.net/tutorials/check-rovo-credits-usage-each-user?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;checking Rovo credits for each user&lt;/a&gt; and for &lt;a href="https://leanzero.net/tutorials/rovo-usage-limit-extra-usage?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;setting or turning off extra usage before 3 December&lt;/a&gt;, if you want to look at real usage first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The assumptions behind the numbers
&lt;/h2&gt;

&lt;p&gt;So you can redo any of this with your own figures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prices are Atlassian's USD list prices read from its pricing pages on 30 October 2026. Everything except Loom carries the 13 October 2026 price change; Loom wasn't part of it, according to Atlassian's &lt;a href="https://www.atlassian.com/licensing/future-pricing/cloud-pricing/faqs" rel="noopener noreferrer"&gt;pricing FAQ&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Annual means the flat price of the user tier your count falls into. Monthly means the graduated per-user bands, times 12.&lt;/li&gt;
&lt;li&gt;On the separate side, Standard means the Standard editions of Jira and Confluence plus Loom Business. Premium means their Premium editions plus Loom Business + AI. That Loom pairing is ours.&lt;/li&gt;
&lt;li&gt;No partner discounts, no Enterprise, no Marketplace apps, no taxes.&lt;/li&gt;
&lt;li&gt;Usage levels (100, 300, 700 and 1,500 credits, and 300 or 1,000 steps per person per month) are assumptions. They aren't measurements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd do before signing
&lt;/h2&gt;

&lt;p&gt;If most of your people would use Loom, take the collection. At our three sizes it's roughly half the cost per seat on Standard and 29% to 50% cheaper on Premium. You get the bigger Rovo and automation pools on top.&lt;/p&gt;

&lt;p&gt;If nobody needs Loom, it comes down to edition and size. On Standard with annual billing it's close to a tie, so pick the collection only if at least half your Jira users also use Confluence (81% at 500 seats). On Premium up to 200 seats the collection is a little cheaper, and its free Guard Standard (worth $8,700 a year at 200 users) makes it the better buy if you want Guard. Above 200 Premium seats, buy the two apps separately unless you were going to pay for Guard Standard anyway or you expect heavy Rovo use across the whole company.&lt;/p&gt;

&lt;p&gt;If you have a big group of Jira-only people (contractors, support staff, an external team), work out where they put you in the tier before you decide. Each one counts as a full collection user, and Atlassian doesn't let you carve them out.&lt;/p&gt;

&lt;p&gt;Whichever you pick, set an extra usage limit in Atlassian Administration before 3 December, because it's on by default. And count who has access to what before you sign. They're all on the bill.&lt;/p&gt;

&lt;p&gt;I'm curious how this looks from your side. If you've moved to Teamwork Collection already, did the invoice match what you expected, and did the Jira-only users catch you out? Tell me in the comments, or write to me at &lt;a href="mailto:gabriela@leanzero.net"&gt;gabriela@leanzero.net&lt;/a&gt; :)&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/blog/teamwork-collection-pricing-vs-jira-confluence-loom?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>atlassian</category>
      <category>jira</category>
      <category>confluence</category>
      <category>loom</category>
    </item>
    <item>
      <title>Atlassian cloud IP ranges 2027: allowlist and webhook IPs</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Fri, 30 Oct 2026 08:59:18 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/atlassian-cloud-ip-ranges-2027-allowlist-and-webhook-ips-ife</link>
      <guid>https://dev.to/mihai_leanzero/atlassian-cloud-ip-ranges-2027-allowlist-and-webhook-ips-ife</guid>
      <description>&lt;p&gt;If your webhook receiver sits behind a firewall, there's probably a list of Atlassian IP addresses pasted into it somewhere, and on 1 January 2027 that list goes out of date. On 16 October Atlassian posted an announcement to its &lt;a href="https://developer.atlassian.com/changelog/" rel="noopener noreferrer"&gt;developer changelog&lt;/a&gt; saying its cloud adds 131.162.88.0/21 to the IP ranges it already uses. Calls from Atlassian to you (webhooks, Connect lifecycle calls and "other requests from Atlassian to your app") can come from 131.162.90.0/23, and calls to Atlassian can go to 131.162.88.0/23.&lt;/p&gt;

&lt;p&gt;This change only adds ranges (Atlassian says they're "in addition to our existing ones"), so nothing stops working before January. After that, a firewall without the new block will drop whatever webhooks Atlassian sends from it. When I tested our own site on 29 October, 0 of 12 requests came from 131.162. And Atlassian's &lt;a href="https://support.atlassian.com/organization-administration/docs/ip-addresses-and-domains-for-atlassian-cloud-products/" rel="noopener noreferrer"&gt;IP addresses and domains&lt;/a&gt; page still says "These addresses were last updated on April 3, 2025" near the top, even though the new block is already in its lists further down. So it's easy to read the page and think nothing moved.&lt;/p&gt;

&lt;p&gt;Most teams will be fine, I think. They add one /23 to one rule. You get caught if you add the wrong half, or if you type in 131.162.0.0/16 because it looks tidier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found in the list, the owner records and our own webhooks
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Atlassian put the 131.162 entries in its machine-readable list by 12 October, before the announcement, and they weren't there in January.&lt;/li&gt;
&lt;li&gt;Atlassian's word "egress" means your inbound. Your webhook firewall needs 131.162.90.0/23.&lt;/li&gt;
&lt;li&gt;Atlassian registered 131.162.88.0/21 and no other part of that /16. A university owns one of the neighbouring blocks.&lt;/li&gt;
&lt;li&gt;Webhooks and Automation on the same site left from two different AWS regions within one minute. Automation used IPv6 for one of its two calls.&lt;/li&gt;
&lt;li&gt;Email, Bitbucket, Trello, Opsgenie and Statuspage have no 131.162 entry at all. Atlassian's notification mail doesn't change.&lt;/li&gt;
&lt;li&gt;Atlassian Administration's IP allowlist is a different list. It decides who can reach your Jira. Atlassian's own addresses only go in it as a workaround, when Automation calls back into your own site.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What changes on 1 January 2027 in the Atlassian cloud IP ranges
&lt;/h2&gt;

&lt;p&gt;Atlassian titled the changelog entry "Atlassian cloud will start using new IP address ranges as we expand our cloud infrastructure", and it's copied into eight product changelogs, Rovo MCP's and Compass's among them. This is the sentence to send your network team:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;From January 1, 2027, Atlassian cloud will add new IP address ranges as we expand our cloud infrastructure. The new ranges are in addition to our existing ones.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It then splits the /21 in two. "Requests to Atlassian (browsers and your clients calling Atlassian APIs): 131.162.88.0/23." And "Requests from Atlassian (webhooks, Connect lifecycle calls, and other requests from Atlassian to your app): 131.162.90.0/23." The instruction is to add them before 1 January 2027, and "If you automate your allowlist updates from this file, no further action is required."&lt;/p&gt;

&lt;p&gt;You'll find the file at &lt;a href="https://ip-ranges.atlassian.com/" rel="noopener noreferrer"&gt;ip-ranges.atlassian.com&lt;/a&gt;. When we read it on 30 October it had syncToken 1793068228, a creationDate of 27 October, and 173 prefixes. Four of them are new:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s https://ip-ranges.atlassian.com/ &amp;gt; ip-ranges.json
$ jq -r '.syncToken, .creationDate, (.items|length)' ip-ranges.json
1793068228
2026-10-27T02:30:28.885679
173
$ jq -r '.items[] | select(.cidr|startswith("131.162."))
    | [.cidr, (.direction|join(",")), (.product|join(","))] | @tsv' ip-ranges.json
131.162.90.20/30    egress  rovo-crawler
131.162.88.0/21 egress,ingress  confluence,forge,jira,loom
131.162.90.0/23 egress  confluence,forge,jira,loom
131.162.88.0/23 ingress confluence,forge,jira,loom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The /23 marked egress is the one your webhook firewall needs, and the /30 for the Rovo crawler sits inside it.&lt;/p&gt;

&lt;p&gt;The changelog and the JSON don't list the same products for the block. In the JSON the block is tagged for four (Jira, Confluence, Forge, Loom). In the changelog it's REST APIs (Jira, Confluence, Cloud Admin, Opsgenie), GraphQL (Compass, Confluence, Jira), Forge and Rovo MCP, and it never mentions Loom. Opsgenie has no 131.162 entry in the file at all. If you allowlist per product, I'd go by the file, because it's the thing Atlassian tells you to automate from.&lt;/p&gt;

&lt;h3&gt;
  
  
  The new block was in the file before the announcement
&lt;/h3&gt;

&lt;p&gt;I found two copies of the file from this year in the Wayback Machine. A January one (26 January, syncToken 1768869839) has 253 prefixes and none under 131.162. An October one (12 October, syncToken 1791782645) already has all four entries, with the same fields as today. So the block went in somewhere between 20 January and 12 October. ARIN's record says Atlassian registered it on 13 April. Since 12 October only three new Bitbucket /32s in eu-central-1 have appeared.&lt;/p&gt;

&lt;p&gt;If your firewall list was copied in January, it's stale in more ways than this one. Between that copy and today, 92 prefixes left the file (88 of them Forge egress /32s) and 12 came in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inbound vs outbound: which half of 131.162.88.0/21 your firewall needs
&lt;/h2&gt;

&lt;p&gt;Atlassian names directions from its own side. Its support page defines an egress connection as "one made by an Atlassian app or service to a server on the internet on your behalf", and lists webhooks, application links to on-premise servers and "checking for new emails on an email server" as examples. Ingress is the other way, "a client, such as a browser, script, app, or SSH client" calling Atlassian. So egress in their file is inbound on your firewall. It's easy to flip this when the rule you're editing is called "inbound".&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you run&lt;/th&gt;
&lt;th&gt;Atlassian's term&lt;/th&gt;
&lt;th&gt;Rule on your side&lt;/th&gt;
&lt;th&gt;Add from 131.162&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Webhook receiver for Jira or Confluence&lt;/td&gt;
&lt;td&gt;egress&lt;/td&gt;
&lt;td&gt;allow inbound from Atlassian&lt;/td&gt;
&lt;td&gt;131.162.90.0/23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint called by Automation "Send web request"&lt;/td&gt;
&lt;td&gt;egress&lt;/td&gt;
&lt;td&gt;allow inbound, IPv4 and IPv6&lt;/td&gt;
&lt;td&gt;131.162.90.0/23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forge Remote backend behind a firewall&lt;/td&gt;
&lt;td&gt;egress&lt;/td&gt;
&lt;td&gt;allow inbound&lt;/td&gt;
&lt;td&gt;131.162.90.0/23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connect app server (lifecycle calls, webhooks)&lt;/td&gt;
&lt;td&gt;egress&lt;/td&gt;
&lt;td&gt;allow inbound&lt;/td&gt;
&lt;td&gt;131.162.90.0/23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On-prem server reached by application links, or a mail server Jira reads from&lt;/td&gt;
&lt;td&gt;egress&lt;/td&gt;
&lt;td&gt;allow inbound&lt;/td&gt;
&lt;td&gt;131.162.90.0/23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corporate proxy or egress firewall that limits where staff can browse&lt;/td&gt;
&lt;td&gt;ingress&lt;/td&gt;
&lt;td&gt;allow outbound to Atlassian&lt;/td&gt;
&lt;td&gt;131.162.88.0/23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scripts or CI calling Atlassian APIs through a destination-filtered firewall&lt;/td&gt;
&lt;td&gt;ingress&lt;/td&gt;
&lt;td&gt;allow outbound to Atlassian&lt;/td&gt;
&lt;td&gt;131.162.88.0/23&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Atlassian's page says that "most admins operating allowlisting systems (such as firewalls, access control lists, and security groups) are only interested in the IP ranges Atlassian apps use to make outgoing connections". That's the first five rows. If you'd rather add the whole /21 to both, the page's own "Atlassian summary IPv4 ranges (preferred)" list now ends with 131.162.88.0/21, so you'd be matching Atlassian's own summary.&lt;/p&gt;

&lt;p&gt;The page also says not to allowlist by region ("include all networks relevant to an app and direction"), and that "Atlassian apps may connect using either IPv4 or IPv6." Our test below hit both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Atlassian owns the /21 and nothing around it
&lt;/h3&gt;

&lt;p&gt;A /21 is 2,048 addresses, 131.162.88.0 to 131.162.95.255. &lt;a href="https://rdap.arin.net/registry/ip/131.162.88.0" rel="noopener noreferrer"&gt;ARIN's record&lt;/a&gt; for it is NET-131-162-88-0-1, a direct allocation to Atlassian Network Services, Inc., registered on 13 April 2026. Other people own the blocks around it: 131.162.8.0 to 131.162.87.255 is Acadia University, 131.162.96.0 to 131.162.111.255 is registered to Joe Wheeler EMC, and 131.162.0.0/22 is registered to someone else again. So please don't round it up. A rule for 131.162.0.0/16 would let the university's network talk to your webhook receiver.&lt;/p&gt;

&lt;p&gt;Mihai's piece on &lt;a href="https://dev.to/blog/atlassian-cloud-security"&gt;Atlassian Cloud edge security&lt;/a&gt; names the two netblocks Atlassian already sends from, 104.192.136.0/21 and 185.166.140.0/22. This is a third one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jira webhook IP ranges and Automation "Send web request": what our test site sent
&lt;/h2&gt;

&lt;p&gt;I wanted to see where these calls come from today, before the new block is used. On 29 October I made a free receiver on webhook.site and pointed two things at it from our test site, wolfaenpak.atlassian.net. One was a Jira admin webhook, created through &lt;code&gt;POST /rest/webhooks/1.0/webhook&lt;/code&gt; for issue created and issue updated in one test project. The other was an Automation rule with one "Send web request" action that fires when an issue is created. Then I created three test issues and edited one of them seven times.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# 12 requests to our test receiver, 29 Oct 2026, from wolfaenpak.atlassian.net (UTC)
time      source IP                                sender                     event
22:44:21  185.166.142.241                          Atlassian Webhook HTTP     jira:issue_created
22:44:27  185.166.142.240                          Atlassian Webhook HTTP     jira:issue_updated
22:46:47  185.166.142.248                          Atlassian Webhook HTTP     jira:issue_created
22:46:49  104.192.142.248                          Automation for Jira        automation
22:47:02  185.166.142.241                          Atlassian Webhook HTTP     jira:issue_updated
22:47:02  185.166.142.241                          Atlassian Webhook HTTP     jira:issue_updated
22:47:03  185.166.142.242                          Atlassian Webhook HTTP     jira:issue_updated
22:47:03  185.166.142.241                          Atlassian Webhook HTTP     jira:issue_updated
22:47:04  185.166.142.241                          Atlassian Webhook HTTP     jira:issue_updated
22:47:05  185.166.142.242                          Atlassian Webhook HTTP     jira:issue_updated
22:47:05  185.166.142.242                          Atlassian Webhook HTTP     jira:issue_created
22:47:07  2401:1d80:321c:5:e21c:bcb1:22a4:ba47     Automation for Jira        automation
# requests from inside 131.162.90.0/23: 0 of 12
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the two Automation lines: same rule, 18 seconds apart, one IPv4 and one IPv6, both from a different region than the webhooks.&lt;/p&gt;

&lt;p&gt;Ten webhook deliveries came from 185.166.142.240, .241, .242 and .248. All four sit in 185.166.142.240/28, which the file tags as egress for eu-west-1. Both Automation calls came from us-east-1: 104.192.142.248 (inside 104.192.142.240/28) and 2401:1d80:321c:5:e21c:bcb1:22a4:ba47 (inside 2401:1d80:321c:5::/64). Their user agent was "Automation for Jira AC app/1.0". None of the 12 came from 131.162.90.0/23. That fits a start date of 1 January.&lt;/p&gt;

&lt;p&gt;So in about three minutes, one site used two regions and both IP versions. It's a small sample from one site, but a rule that lists only the few Atlassian addresses you've seen in your logs would already have missed some of these. Check your IPv6 rules. If your endpoint has an IPv6 address (an AAAA record) and your firewall has no IPv6 rule for Atlassian, Automation calls like the second one would already be hitting it.&lt;/p&gt;

&lt;p&gt;I couldn't find a support page just for Automation's outgoing IPs (the URL you'd guess returns a 404). Atlassian's &lt;a href="https://developer.atlassian.com/cloud/jira/platform/webhooks/" rel="noopener noreferrer"&gt;webhooks documentation&lt;/a&gt; only says "You need to allow-list IP address ranges for outgoing connections to ensure webhook delivery", which sends you back to the same list.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F0007c823565b38fad9b86a90762aa3b3907de6fa-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F0007c823565b38fad9b86a90762aa3b3907de6fa-1600x896.png" title="Illustration, generated locally with ERNIE-Image. A firewall with last year's list turns away a webhook from 131.162.90.0/23 the same way. Its address just wasn't on the clipboard." alt="A cartoon of a stern silver robot bouncer holding a clipboard at a velvet rope while a cheerful blue delivery robot with a parcel waits and a queue of other robots looks on" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I was done I deleted the webhook (the GET after it returned 404), the Automation rule, the three issues and the webhook.site token, so the test site is back to how it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forge remotes and Connect apps: what your backend must allow
&lt;/h2&gt;

&lt;p&gt;Forge remotes and Connect app servers need 131.162.90.0/23 too. Forge's &lt;a href="https://developer.atlassian.com/platform/forge/remote/essentials/" rel="noopener noreferrer"&gt;Remote essentials&lt;/a&gt; page says: "When communicating with remote backends, Forge uses IP address ranges as indicated in the Outgoing Connections section". Forge is one of the four products tagged on the 131.162 entries, so a remote behind a firewall needs 131.162.90.0/23 like any webhook receiver. Connect is named too. "Connect lifecycle calls" are in the changelog, so a Connect app server needs the same /23 for its install and uninstall callbacks.&lt;/p&gt;

&lt;p&gt;The support page also warns that these outgoing ranges "include traffic that is untrusted and user-configurable, for example the Forge platform." Anyone can write a Forge app. Its fetches leave from the same Atlassian addresses. An allowlist tells you a request came from Atlassian's network. It doesn't tell you which customer or which app sent it, so your backend still has to check the JWT or the signature on every call.&lt;/p&gt;

&lt;p&gt;For Connect apps the new range starts a month before &lt;a href="https://dev.to/blog/atlassian-migrations-the-deadline-nobody-is-watching"&gt;Connect end of support on 31 January 2027&lt;/a&gt;. If you're still running a Connect app server in January, it needs the new /23 from 1 January and for as long as it keeps running after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Email, Bitbucket Pipelines and the Rovo crawler IP ranges: only the crawler gets a 131.162 entry
&lt;/h2&gt;

&lt;p&gt;Email isn't affected. In the file 24 entries are tagged email. They match the support page's list of notification senders exactly (I diffed them), and not one is a 131.162 address. Email has had its own moves before. In May 2025 Atlassian posted &lt;a href="https://community.atlassian.com/forums/Jira-articles/Atlassian-emails-will-be-coming-from-new-ips/ba-p/3019891" rel="noopener noreferrer"&gt;Atlassian emails will be coming from new ips&lt;/a&gt; (4,213 views), and in August a follow-up, &lt;a href="https://community.atlassian.com/forums/Jira-articles/Atlassian-emails-IP-range-changes/ba-p/3089189" rel="noopener noreferrer"&gt;Atlassian emails - IP range changes&lt;/a&gt;, which has 3,111 views and no comments. Both of those ranges are still in the email list.&lt;/p&gt;

&lt;p&gt;One mail rule does change. If Jira or Jira Service Management reads mail from your own mail server, Atlassian counts that as egress ("checking for new emails on an email server"), so that server's firewall needs 131.162.90.0/23 like a webhook receiver does.&lt;/p&gt;

&lt;p&gt;Bitbucket has 100 entries in the file, with no 131.162 among them. Its &lt;a href="https://support.atlassian.com/bitbucket-cloud/docs/what-are-the-bitbucket-cloud-ip-addresses-i-should-use-to-configure-my-corporate-firewall/" rel="noopener noreferrer"&gt;firewall page&lt;/a&gt; sends you to the same JSON and to the Outgoing Connections list for webhooks. Pipelines runner IPs are a separate list. Trello would rather you didn't use IPs: its &lt;a href="https://support.atlassian.com/trello/docs/configuring-your-firewall-for-trello/" rel="noopener noreferrer"&gt;firewall page&lt;/a&gt; says "Trello's IP addresses can change frequently and without notice" and suggests domain names instead. Opsgenie and Statuspage are in the file too. Neither has a 131.162 entry.&lt;/p&gt;

&lt;p&gt;Rovo's crawler has its own 131.162 entry: 131.162.90.20/30, egress, in gcp-us-central1. It's the only 131.162 entry with a region that isn't global, and it sits inside the egress /23. If you let Rovo's crawler into an internal site you've connected, check that the /30 (or the /23) is in that rule. The support page lists 14 crawler ranges and the file has 13. The page's extra one, 104.192.138.224/28, left the file this year.&lt;/p&gt;

&lt;p&gt;Loom is tagged on all three big 131.162 entries in the file, but Loom's own &lt;a href="https://support.atlassian.com/loom/kb/domains-protocols-and-ports-required-for-loom-to-work-on-your-servers/" rel="noopener noreferrer"&gt;domains and ports article&lt;/a&gt; only lists domains and points you to AWS's published ranges. If your proxy restricts Loom by IP, I'd add 131.162.88.0/23 for it too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Atlassian Guard IP allowlist is a different list from your firewall
&lt;/h2&gt;

&lt;p&gt;In Atlassian Administration, under Security, Device security, IP allowlists, you can create a list of the addresses your people must use to reach Jira or Confluence. The page says "Powered by Atlassian Guard", but the plan Atlassian's doc asks for is the app's: Premium for Jira, Jira Service Management, Confluence and Compass. An org admin sets it up. Atlassian's &lt;a href="https://support.atlassian.com/security-and-access-policies/docs/specify-ip-addresses-for-product-access/" rel="noopener noreferrer"&gt;docs&lt;/a&gt; put it this way: "Use an IP allowlist to specify which IP addresses or locations users must use to access content".&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fb263e6beffa0fc460eccaa655a8cb2af0869fc3f-1440x1000.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fb263e6beffa0fc460eccaa655a8cb2af0869fc3f-1440x1000.png" title="Our test org's IP allowlists page on 30 October 2026, with 0 lists. This list controls who reaches your apps. Atlassian's addresses only belong here if Automation calls back into your own site." alt="The IP allowlists page in Atlassian Administration for the wolfaenpak organisation, Powered by Atlassian Guard, with the sidebar open at Security, Device security, IP allowlists, a Create IP allowlist button and an empty table reading No IP allowlist created" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That list is about your users' addresses, so for most orgs the 2027 change needs nothing there. Where the two do meet is when Atlassian calls itself. A thread on the developer community, &lt;a href="https://community.developer.atlassian.com/t/jira-cloud-ip-allowlisting-and-automation-web-request/72040" rel="noopener noreferrer"&gt;Jira Cloud IP Allowlisting and Automation Web Request&lt;/a&gt; (2,277 views), describes an Automation rule that calls its own site's REST API and fails once the allowlist is on. A request to exclude Atlassian's own IPs automatically, &lt;a href="https://jira.atlassian.com/browse/ACCESS-1002" rel="noopener noreferrer"&gt;ACCESS-1002&lt;/a&gt;, has 34 votes and is still gathering interest. Its workaround is to add Atlassian's IPs to the allowlist by hand. If you did that, add 131.162.90.0/23 there too before 1 January, or the Automation calls that leave from the new block get rejected. If you use Guard together with &lt;a href="https://dev.to/blog/atlassian-data-security-policy-guard"&gt;data security policies&lt;/a&gt;, it's a good moment to re-check which rules call back into your own site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Atlassian IP allowlist from the ip-ranges.atlassian.com JSON: a script you can schedule
&lt;/h2&gt;

&lt;p&gt;Atlassian's line is that if you build your allowlist from the file, you don't need to do anything. So I'd stop pasting the list by hand. I ran this small script on a Mac on 30 October. It downloads the file, keeps every egress prefix tagged for Jira, Confluence or Forge (IPv4 and IPv6), and prints what was added or removed since the last run, using the syncToken to tell whether anything changed. The Automation addresses from our test sit in prefixes tagged jira, so this filter covers them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# Builds the list of CIDRs that Jira, Confluence and Forge call your servers from,&lt;/span&gt;
&lt;span class="c"&gt;# and tells you what changed since the last run. Needs curl and jq.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail
curl &lt;span class="nt"&gt;-sf&lt;/span&gt; https://ip-ranges.atlassian.com/ &lt;span class="nt"&gt;-o&lt;/span&gt; ip-ranges.json
&lt;span class="nv"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; .syncToken ip-ranges.json&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;last&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;last-token.txt 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;none&lt;span class="si"&gt;)&lt;/span&gt;

jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.items[]
  | select(.direction | index("egress"))
  | select(.product | any(. == "jira" or . == "confluence" or . == "forge"))
  | .cidr'&lt;/span&gt; ip-ranges.json | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; allow-new.txt

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$last&lt;/span&gt;&lt;span class="s2"&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;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"no change (syncToken &lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"syncToken &lt;/span&gt;&lt;span class="nv"&gt;$last&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;touch &lt;/span&gt;allow.txt
  &lt;span class="nb"&gt;comm&lt;/span&gt; &lt;span class="nt"&gt;-13&lt;/span&gt; allow.txt allow-new.txt | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^/add    /'&lt;/span&gt;
  &lt;span class="nb"&gt;comm&lt;/span&gt; &lt;span class="nt"&gt;-23&lt;/span&gt; allow.txt allow-new.txt | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^/remove /'&lt;/span&gt;
  &lt;span class="nb"&gt;mv &lt;/span&gt;allow-new.txt allow.txt
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; last-token.txt
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &amp;lt; allow.txt | xargs &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"prefixes in allow.txt:"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run it prints all 96 prefixes as adds (39 IPv4 and 57 IPv6), with the two new ones among them. When I ran it again straight away, it only reported that nothing changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ ./atlassian-egress.sh
syncToken none -&amp;gt; 1793068228
add    104.192.136.0/21
add    104.192.136.0/24
add    104.192.136.240/28
...
add    131.162.88.0/21
add    131.162.90.0/23
...
prefixes in allow.txt: 96
$ ./atlassian-egress.sh
no change (syncToken 1793068228)
prefixes in allow.txt: 96
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check the "what changed" part I set the saved token back and took the two 131.162 lines out of allow.txt, and the next run printed exactly &lt;code&gt;add    131.162.88.0/21&lt;/code&gt; and &lt;code&gt;add    131.162.90.0/23&lt;/code&gt;. What you do with allow.txt depends on your firewall (a security group, an nginx &lt;code&gt;allow&lt;/code&gt; list, a ticket to the network team). I'd at least have a person read the add and remove lines before anything gets pushed automatically. Read the &lt;code&gt;remove&lt;/code&gt; lines too: since January, 92 prefixes have left the file.&lt;/p&gt;

&lt;p&gt;If you don't want to poll, Atlassian publishes changes to an SNS topic, &lt;code&gt;arn:aws:sns:us-east-1:745490931007:atlassian-public-ip-changes&lt;/code&gt;. Each message carries the new syncToken, an md5 of the file and its URL, so you can trigger the same script from 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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F3929c3714c948c4007555c68a5d1d761e1d690d7-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F3929c3714c948c4007555c68a5d1d761e1d690d7-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Copying 173 prefixes onto a firewall by hand, next to a short script that fetches them. The robot with the coffee got the IPv6 ones too." alt="A cartoon of a sweating blue robot on a ladder painting numbers onto a long paper scroll pinned to a brick wall while a relaxed robot sips coffee next to a small machine printing numbers onto paper" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do before 1 January
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Search your firewall rules and security groups with &lt;code&gt;grep -E '104\.192\.1(3[6-9]|4[0-3])\.|185\.166\.14[0-3]\.|2401:1d80:'&lt;/code&gt;. That finds Atlassian's 104.192.136.0/21, 185.166.140.0/22 and 2401:1d80::/32, including single addresses from those ranges that someone copied out of a log. A hit is probably a hand-typed Atlassian list, but check its direction. An inbound rule that lets Atlassian call you gets 131.162.90.0/23. An outbound rule to Jira or Confluence gets 131.162.88.0/23. A rule that only covers Bitbucket needs nothing.&lt;/li&gt;
&lt;li&gt;Add 131.162.90.0/23 (or the whole /21) to every inbound rule that lets Atlassian call you, which is the five egress rows in the table above. Leave the /16 alone.&lt;/li&gt;
&lt;li&gt;If you restrict where your own people can browse, add 131.162.88.0/23 for the Atlassian apps they use.&lt;/li&gt;
&lt;li&gt;If your endpoints have IPv6 addresses, check that those rules also have Atlassian's IPv6 ranges, because Automation already used IPv6 in our test.&lt;/li&gt;
&lt;li&gt;Move the list to a scheduled job built from ip-ranges.atlassian.com, like the script above, with a person reading the diff.&lt;/li&gt;
&lt;li&gt;Leave the mail filters for Atlassian's notification mail alone (a mail server Jira reads from is covered in step 2). Leave your Guard IP allowlist alone too, unless you added Atlassian's IPs to it so Automation can call your own site. In that case add 131.162.90.0/23 there as well.&lt;/li&gt;
&lt;li&gt;In the first week of January, send a test webhook to a receiver outside your firewall and check the source address.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Have you got a firewall somewhere with a list someone copied years ago? Or a reason you can't sync from the JSON (I know some network teams won't let a script near their rules, and I'd like to hear how you handle it)? Tell me in the comments.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/blog/atlassian-cloud-ip-ranges-2027?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>jira</category>
      <category>atlassian</category>
      <category>security</category>
      <category>webhooks</category>
    </item>
    <item>
      <title>Remove Inactive Users in Jira Cloud with a CogniRunner Agent</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Fri, 30 Oct 2026 06:02:33 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/remove-inactive-users-in-jira-cloud-with-a-cognirunner-agent-4fj9</link>
      <guid>https://dev.to/mihai_leanzero/remove-inactive-users-in-jira-cloud-with-a-cognirunner-agent-4fj9</guid>
      <description>&lt;p&gt;This tutorial sets up a CogniRunner agent to remove inactive users in Jira Cloud by a rule you type in plain words: at most 10 of your own users per product, least active first, and a person approves each removal. You end up with a virtual administrator that counts seats through Atlassian's Organizations REST API, files a removal request when a product goes over the limit, and changes nobody's access until an organisation admin says yes.&lt;/p&gt;

&lt;p&gt;We filmed the video below on 9 October on CogniRunner 1.32.2. On 29 October we repeated it on our test site with 1.35.15, and the screenshots here come from that second run. One approved request took Confluence on the site from 9 licensed users to 8 by Atlassian's own count, and Atlassian answered 204. It wasn't a clean run. Neither of our freshly described agents posted a report, and the removal only came after we pasted in a plan an administrator had written for the video, which the video's on-screen rail also admits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=NKbEv_MoPRo" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=NKbEv_MoPRo&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Jira Cloud site with &lt;a href="https://marketplace.atlassian.com/apps/298437877/cognirunner?hosting=cloud" rel="noopener noreferrer"&gt;CogniRunner&lt;/a&gt; installed (we used 1.35.15), and you as a CogniRunner admin.&lt;/li&gt;
&lt;li&gt;An organisation admin in Atlassian Administration. CogniRunner only lets an organisation admin approve an organisation operation.&lt;/li&gt;
&lt;li&gt;An organisation API key and its organisation ID, from Atlassian Administration. Our first key was no longer valid, so test yours before you paste it.&lt;/li&gt;
&lt;li&gt;An AI provider saved in CogniRunner Settings. Describe a job runs on the model in Model for validators and rules, and its first step, finding the operations, has about 20 seconds. For us openai/gpt-6-luna timed out twice at that step, and anthropic/claude-sonnet-5.5 produced the whole proposal in 29 s.&lt;/li&gt;
&lt;li&gt;A Jira project only admins can see, for the agent's daily review issue. We made a Kanban project with the key LICW.&lt;/li&gt;
&lt;li&gt;About 100,000 AI tokens for a first day of testing. Our two agents used 18,839 and 77,499.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;You type the rule and press Create, and CogniRunner turns it into an agent with a list of allowed Atlassian operations and a daily job. The agent reads whatever it needs, but each removal becomes a request, and only an organisation admin's Approve and run sends it.&lt;/p&gt;

&lt;p&gt;I'll use three words a lot below. A tick is one pass the agent makes over its review issue, on a schedule or when you press Run a tick now. A tick works in rounds, one model call each. Each tick leaves a block in the agent's Scratchpad tab.&lt;/p&gt;

&lt;p&gt;[[steps]]&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Store the organisation API key&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Describe the job in plain words&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review the setup the agent proposes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create the agent and read the Create list&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run today's review now and read the scratchpad&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Count seats per product through the Organizations REST API&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run a drill on one product with a lower limit&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Approve the request to remove the user from the group, and check the 204&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Count again, put the person back, and delete the drill&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Jira Cloud can't automatically disable inactive users on its own (AX-1169, 784 votes)
&lt;/h2&gt;

&lt;p&gt;Atlassian bills you on "the number of users with app access at the end of your billing cycle". Whether those people opened Jira this year doesn't come into it. And the fix Atlassian documents on its user-count page is manual: "Use the Export feature to identify the inactive users, and then manually revoke their access one by one on this page."&lt;/p&gt;

&lt;p&gt;People have asked for a schedule since 2019. AX-1169, "Deactivate inactive user based on activity", has 784 votes and is still Under Consideration (read on 29 October 2026). ACCESS-720, about deactivation policies for managed accounts, has 214 votes and says Gathering Interest. So Jira Cloud has no built-in setting for it yet. Both requests are still open. Marketplace apps, ours included, read the data and call the API on your behalf.&lt;/p&gt;

&lt;p&gt;If you only want to find the inactive people, &lt;a href="https://leanzero.net/blog/find-inactive-users-jira-cost-slasher?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;find inactive users first&lt;/a&gt; with the free export method. What an idle seat costs at 50, 200 or 500 users is in &lt;a href="https://leanzero.net/blog/atlassian-usage-based-pricing-budget-2027?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;our Atlassian pricing budget&lt;/a&gt;. Why we'd put an agent on this at all is argued in the &lt;a href="https://leanzero.net/blog/atlassian-license-management-cognirunner-ai-agent?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Atlassian license management post&lt;/a&gt;, which embeds this video beside two others.&lt;/p&gt;

&lt;p&gt;One thing to settle before Step 1. The agent removes a user from the group that gives them a product, and that ends the access the group gave. It does not suspend, deactivate or delete the Atlassian account. Atlassian's API docs warn that "the user may still be in other groups that grant the same app access". Atlassian also says you "won't be billed for a user once they're removed" from the organisation. For someone who only lost one product I'd expect that product's count to drop (ours did, 9 to 8), but we couldn't find one Atlassian sentence that says so per product. The same call removes Jira access when the group is the one that grants Jira; our measured removal was in Confluence.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F968588def3cd42c9c16c8439a7815f9a131ea646-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F968588def3cd42c9c16c8439a7815f9a131ea646-1600x896.png" title="Illustration, generated locally with ERNIE-Image. The rule keeps each product at 10 people, removes the least active first and never touches an admin. The pyjamas are a guess about last active dates." alt="Illustration of a robot bouncer with a clipboard at a velvet rope under a MAX 10 sign, waving a sleepy robot in pyjamas aside while a robot wearing a crown walks past" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Store the organisation API key
&lt;/h2&gt;

&lt;p&gt;Open CogniRunner from Jira's Apps menu and go to Agents, then Setup. Find the Connections &amp;amp; keys card. Under Organisation API key, fill in the Organisation ID and the API key, plus an Expires on date if you want one. Tick the box that lets CogniRunner store the credential and press Check and store.&lt;/p&gt;

&lt;p&gt;CogniRunner checks the key with Atlassian before it stores anything. Our first key was no longer valid, and the card answered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Atlassian refused this key (HTTP 401). Check that it is current and was created for this organisation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A current key went in fine, and the card switched to "in use", with Replace and Remove next to it. That's your check for this step. The key field is write-only, so nobody can read the key back out of the page later. I'd create a key just for this agent anyway. Then you can revoke it without breaking anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Describe the job in plain words
&lt;/h2&gt;

&lt;p&gt;Go back to Virtual administrators under Agents and press Describe a job. Name the agent and pick its home project, then set the time zone. About the home project, the form says: "The home project is where it opens a review issue every day and reports on it. Use a project only admins can see."&lt;/p&gt;

&lt;p&gt;Then type the job. This is the rule from the video, word for word. It's 584 characters, and the form takes 20 to 4,000:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Keep our own users at no more than 10 per product, on every site of this organisation. Demo evaluators don't count and are never removed: they are the people in the Demo evaluator field of DACC issues not in Demo expired, and LeanZero offboards them after 5 days. Each day, count the licensed users of every product, and where a product is over 10, propose removing the least active people first, by their last active dates, from the group that gives them the product. Never touch organisation admins, site admins, or app and service accounts. Tell me what you found and what you did.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That evaluator sentence is ours. LeanZero lets demo visitors into one site and offboards them after five days, so they shouldn't count. Replace it with your own exceptions or delete it. I'd name exceptions by something the agent can read, a field on an issue or a group, because "contractors" means nothing to an API.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F7a176ce582d346a159894cf2547f4e50f3fb531a-1080x528.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F7a176ce582d346a159894cf2547f4e50f3fb531a-1080x528.png" title="CogniRunner 1.35.15 on our test site. Under the title it says changes to users, groups or configuration still wait for your approval every time." alt="CogniRunner's Describe the job form with the name Rolling licences, home project LICW, time zone Europe/Bucharest and the license rule typed in" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press Ask the agent to propose its setup. You should see the proposal in under a minute; ours took 29 seconds with Claude Sonnet 5.5. With GPT-6 Luna it failed twice at about 21 seconds: "The AI provider did not answer in time, so no setup was proposed. Ask again." Switching the Agent model changed nothing. The first step runs on Model for validators and rules, so that's the slot to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Review the setup the agent proposes
&lt;/h2&gt;

&lt;p&gt;What comes back is a list of Atlassian operations and a plan with a schedule. I'd read the operations first. They decide what the agent can do at all. I wouldn't create an agent whose list I hadn't read.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F54703f5c64bca5069c225879c1b2ccc65a858f29-1046x610.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F54703f5c64bca5069c225879c1b2ccc65a858f29-1046x610.png" title="Each row names the REST call it maps to. Only the DELETE on a group membership is orange." alt="Eight Atlassian operations proposed by the agent, seven marked Runs on its own and Remove user from group marked Asks you each time" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ours asked for 8 operations: get directories, list workspaces (sites and products), search users, last active dates, get groups, get group role assignments, a JQL search for the evaluators, and Remove user from group. Seven say Runs on its own. I like that. Remove user from group says Asks you each time, and that label isn't just a badge: when our drill agent reached that call, its tick ended with "That needs a person's approval first, so it was not done in this turn." If your list has any other operation that writes, stop and change the description before you create anything.&lt;/p&gt;

&lt;p&gt;In the video the agent asked for &lt;code&gt;getDirectoryUsersCount&lt;/code&gt; and &lt;code&gt;getDirectoryUsers&lt;/code&gt;. Ours asked for &lt;code&gt;searchDirectoryUsers&lt;/code&gt; instead. Both read the same people, but it means the list is picked fresh from your words each time you describe a job.&lt;/p&gt;

&lt;p&gt;Our plan came back as ten numbered steps. It reads the review issue and lists the directories. Then it counts each product's users per site and ranks the people over the limit by &lt;code&gt;last_active&lt;/code&gt;, oldest first. It finds each person's groups that grant the product, skips groups an identity provider manages, and files the removals together at the end.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F09a1fd40fade93f6ea6ea1c7a2ff76aa8eab6098-1046x347.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F09a1fd40fade93f6ea6ea1c7a2ff76aa8eab6098-1046x347.png" title="Step 5 asks for last active dates one person at a time. Step 7 skips any group whose managementAccess.modifiable is false, since a synced group would undo the removal." alt="The proposed plan in ten numbered steps, from reading the review issue to filing removal calls and recounting" width="799" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two details in the plan affect your bill. Atlassian counts someone as active after "viewing a product's page for a minimum of 2 seconds", and the date "may be delayed by up to 24 hours". So a person who glanced at a Confluence page last week looks active, and someone who logged in this morning may still show yesterday's date. A person with no last active date for the product falls back to the date they joined the organisation, never to today. I'd keep a margin of a week or two in any rule built on these dates. It's cheap insurance.&lt;/p&gt;

&lt;p&gt;And the schedule line said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;It opens a review issue in LICW every day at 07:00 (Europe/Bucharest), labelled cr-review-rolling-licences, and checks for work every 30 minutes, at any hour. It also reads DACC. It may spend up to 500,000 tokens a day, and a second check reads each report before it is posted.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How the agent decides who goes
&lt;/h2&gt;

&lt;p&gt;CogniRunner ships a rolling-licences knowledge pack that the agent plans with. Here's what it says about who counts and who can be removed, quoted where the wording matters.&lt;/p&gt;

&lt;p&gt;A billable seat is "an ACTIVE user who holds the role &lt;code&gt;atlassian/user&lt;/code&gt; or &lt;code&gt;atlassian/admin&lt;/code&gt; on that product's resource". Guests, customers, stakeholders and user-access admins don't count. A suspended user holds no seat "even when a group still grants the role". And "Group membership is not product access". So the count call in Step 6 filters on roles.&lt;/p&gt;

&lt;p&gt;Anyone whose platform roles include &lt;code&gt;atlassian/org-admin&lt;/code&gt; or &lt;code&gt;atlassian/site-admin&lt;/code&gt; is counted in the total and never proposed. So is any account whose &lt;code&gt;accountType&lt;/code&gt; isn't &lt;code&gt;atlassian&lt;/code&gt;. Apps and service accounts show up that way.&lt;/p&gt;

&lt;p&gt;Least active first means "the oldest last-active date, then the oldest join date". A group synced from an identity provider is skipped, because "a removal would be undone". I think that last rule is the one homemade scripts most often miss: they remove the person, the next sync puts them back, and the seat never goes away.&lt;/p&gt;

&lt;p&gt;But the admin protection only covers admins CogniRunner can recognise. On the organisation door it recognises them by platform roles. Without an organisation key, an organisation admin who sits in none of the admin groups isn't recognised on a site door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4. Create the agent and read the Create list
&lt;/h2&gt;

&lt;p&gt;Press Create. It took 12 seconds. You get one line per step:&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fe43d27102d2e4eb1e1ca0464cade2d4a78ac0d7b-1046x109.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fe43d27102d2e4eb1e1ca0464cade2d4a78ac0d7b-1046x109.png" title="Our test site has no DACC project, so the evaluator search was dropped. Read any red line on your site before you go on." alt="The Create list: created Rolling licences with 8 operations, a red line saying project DACC was dropped, 11 standing rules pinned, the daily job scheduled, moved to Live" width="796" height="83"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That red line comes from our setup. Our rule mentions DACC, a project that only exists on our production demo site, so CogniRunner dropped the JQL search it could never run and the agent kept 7 operations. The fourth line scheduled "Rolling licences: open the daily review" at 0 7 * * *. Ours ended "Moved it to Live", because we ran 1.35.15. From 1.35.16 (29 October) every new agent starts in Learn, and your last line will say so: "It starts in Learn: it reads and takes notes and changes nothing until you promote it on its page." An agent in Learn can't file a removal request. Move it up first.&lt;/p&gt;

&lt;p&gt;Open the agent from its card, click Live on the phase strip under its name, and confirm Move to Live. Live in CogniRunner means the agent may post and act within its powers. Here those powers contain one write, and it asks each time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5. Run today's review now and read the scratchpad
&lt;/h2&gt;

&lt;p&gt;Go to Scheduled Jobs and press Run now on "Rolling licences: open the daily review". You should see today's review issue appear in the home project (ours was LICW-1). Then open the agent from its card and press Run a tick now. On an empty Activity pane the button sits in the middle. Once the pane has content, it's only in the More actions menu next to Pause.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F87ac83d5eb4290f027fbdf417762ee1ad8bd2fce-1120x701.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F87ac83d5eb4290f027fbdf417762ee1ad8bd2fce-1120x701.png" title="CogniRunner Admin v1.35.15. The line under the name lists the operations the agent may call and says the removal waits for an organisation admin's approval." alt="The agent's page: name Rolling licences, the phase strip with Live selected, the Activity pane empty with Run a tick now" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A confirm dialog says the tick "sweeps its intake now instead of at its next scheduled run" and uses its own budget. After a minute or two the Scratchpad tab gets a new block. Ours said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The turn ended when it said it was done, after 2 rounds, in the Live phase. It staged nothing and changed nothing. Not done: stage a reply. The draft broke one of the writing rules, so it was refused and not staged.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So our fresh agent posted no report, and the tick used 18,839 tokens. Part of that is our site: its plan stops if the evaluator search returns nothing, and that search had been dropped. The refused draft is a product problem, though, and the screen doesn't say which rule it broke. We wrote it up for the CogniRunner team the same day. My advice until it's fixed: don't wait for the report. Watch the Atlassian calls tab. The video shows a watchdog an administrator had already tightened by hand, and you'll probably need that one round of tightening too.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fb698c7a56b8ef9c55e552f69fea10ec240191467-784x266.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fb698c7a56b8ef9c55e552f69fea10ec240191467-784x266.png" title="Each line says whether the engine or the model wrote it. Both of these are the engine's." alt="Scratchpad of the first tick: swept 1 candidate, then the turn ended after 2 rounds having staged nothing because the draft broke a writing rule" width="784" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6. Count seats per product through the Organizations REST API
&lt;/h2&gt;

&lt;p&gt;Atlassian's own numbers are a few calls away, and you want them anyway to check whatever the agent tells you. Export your key and organisation ID first (&lt;code&gt;ORG_API_KEY&lt;/code&gt;, &lt;code&gt;ORG_ID&lt;/code&gt;), then find the directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ORG_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.atlassian.com/admin/v2/orgs/&lt;/span&gt;&lt;span class="nv"&gt;$ORG_ID&lt;/span&gt;&lt;span class="s2"&gt;/directories"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"data":[{"directoryId":"5fe8d258-49f8-40df-9b65-7f4ab5581af1","name":"wolfaenpak","icon":""}],"links":{...}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then list the sites and products. Each item's &lt;code&gt;id&lt;/code&gt; is the product's resource ID. The count call needs it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ORG_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.atlassian.com/admin/v2/orgs/&lt;/span&gt;&lt;span class="nv"&gt;$ORG_ID&lt;/span&gt;&lt;span class="s2"&gt;/workspaces"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"data":[{"id":"ari:cloud:confluence::site/&amp;lt;siteId&amp;gt;","attributes":{"name":"...","typeKey":"confluence","directory":"5fe8d258-...",...}},...],"links":{"next":"..."},"meta":{"pageSize":20,"total":90}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;DIRECTORY_ID&lt;/code&gt; to the &lt;code&gt;directoryId&lt;/code&gt; above and &lt;code&gt;SITE_ID&lt;/code&gt; to the part after &lt;code&gt;site/&lt;/code&gt; in your Confluence item's &lt;code&gt;id&lt;/code&gt;. Your first item may be another app. It pages 20 at a time: for the next 20, send &lt;code&gt;-d '{"cursor":"&amp;lt;links.next&amp;gt;"}'&lt;/code&gt;. Our organisation listed 90 workspaces over 5 pages, Jira, Confluence and JSM among other app types. Now count one product's licensed users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ORG_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.atlassian.com/admin/v2/orgs/&lt;/span&gt;&lt;span class="nv"&gt;$ORG_ID&lt;/span&gt;&lt;span class="s2"&gt;/directories/&lt;/span&gt;&lt;span class="nv"&gt;$DIRECTORY_ID&lt;/span&gt;&lt;span class="s2"&gt;/users/count?resourceIds=ari%3Acloud%3Aconfluence%3A%3Asite%2F&lt;/span&gt;&lt;span class="nv"&gt;$SITE_ID&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;roleIds=atlassian%2Fuser&amp;amp;roleIds=atlassian%2Fadmin&amp;amp;status=active"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"count":9}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's Confluence on our main test site. Both &lt;code&gt;roleIds&lt;/code&gt; and &lt;code&gt;status=active&lt;/code&gt; follow the knowledge pack's seat rule above. I trust these numbers more than any agent's summary. Ours included. Done for every Jira, Confluence, JSM and Customer Service Management product, our organisation came to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;17 sites, 34 products, HTTP 200 on every call (29 October 2026)
Jira                            17 products   3 to 8 licensed users
Confluence                      12 products   3 to 9 licensed users
Jira Service Management          3 products   3 to 5 licensed users
Customer Service Management      2 products   3 licensed users each
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We counted the &lt;code&gt;jira-software&lt;/code&gt; workspaces as Jira. The call also lists a &lt;code&gt;jira&lt;/code&gt; workspace for each of the 17 sites, at 3 to 4 users each, which we left out of the 34; we didn't find a source that says which of the two is the billed seat. I'd count both on your site until you know. In the video the same organisation had 32 products holding 3 to 6 users each. Nothing was over 10 either time, so our real rule removes nobody. That's why the video and this tutorial both run a drill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7. Run a drill on one product with a lower limit
&lt;/h2&gt;

&lt;p&gt;I'd always drill first. Pick a product where you already know who should be picked. Describe a second job, Licence drill, with a limit that forces one removal on one product. Ours was Confluence on the wolfaenpak site. Limit 8:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Keep our own users at no more than 8 on Confluence on the wolfaenpak site of this organisation; this is a drill, check only that one product on that one site. Count its licensed users, and if it is over 8, propose removing the least active people first, by their last active dates, from the group that gives them the product. Never touch organisation admins, site admins, or app and service accounts. Tell me what you found and what you did.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its proposal took 24 seconds and asked for 9 operations. I'd check those the same way as before, and I'd look for the removal's orange label first. Create came back with four green lines and no red one. On 1.35.16 or later, move the drill to Live on its page too. Do it before the first tick.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F9f3f9114c3d4fb533af5c4a071020d709573b9e8-1046x73.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F9f3f9114c3d4fb533af5c4a071020d709573b9e8-1046x73.png" title="No DACC in this description, so nothing was dropped." alt="Create list for Licence drill: 9 operations allowed, 12 standing rules pinned, a daily job at 08:00, moved to Live" width="800" height="56"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first two ticks failed.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The first tick ran 8 rounds and stopped with "Not done: call an Atlassian operation. Atlassian answered 400 and refused it." We didn't see which parameter Atlassian refused. We opened the agent's settings, chose Job and then Edit, and replaced its "How I will do it" with the plan an administrator had refined for the video (below). Under Atlassian calls we pressed Change the list and allowed one more operation, Get users in an organization.&lt;/li&gt;
&lt;li&gt;The second tick said nothing had changed on the issue since it last looked, and queued nothing. A tick only works an issue that changed, so we commented on the review issue (LICW-2) and ran the tick again from More actions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the plan we pasted, shortened, with our IDs swapped for placeholders. It writes the IDs in so the agent doesn't have to find them, and our limit was 8 where the video's was 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Known values: orgId = &amp;lt;orgId&amp;gt;; directoryId = &amp;lt;directoryId&amp;gt;; the Confluence resource on the site = ari:cloud:confluence::site/&amp;lt;siteId&amp;gt;. No other call is needed to find them.
1. FIRST round, both at once: org.getDirectoryUsersCount and org.getDirectoryUsers, each with pathParams {"orgId", "directoryId"} and query {"resourceIds": "&amp;lt;the resource&amp;gt;", "roleIds": ["atlassian/user", "atlassian/admin"], "status": "active"}; getDirectoryUsers also takes "limit": 100.
2. SECOND round: leave out everyone whose platformRoles holds atlassian/org-admin or atlassian/site-admin, and every accountType that is not atlassian. For EACH person left, both at once: org.getDirectoryUsersLastActiveDates and org.getGroups (that person's groups for the resource).
3. THIRD round: rank those people by last_active on the Confluence resource, oldest first (no date = never active = first). Take only as many as bring the count down to 8. For each one, file org.deleteDirectoriesGroupsMemberships for the group whose managementAccess.modifiable is true, all in this one round. Its reason names the person, their last-active date, the group, and the admins you left out.
4. FOURTH round: stage ONE comment on the review issue: the count before, who is protected and why, who you proposed and their last-active dates, how many requests wait for approval. Never finish without staging it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our third tick ran 5 rounds and ended with "Not done: call an Atlassian operation. That needs a person's approval first, so it was not done in this turn." A writing rule refused the report draft again, even with the edited plan. The request was filed anyway, and you should see "Waiting for your approval 1" on the agent's Atlassian calls tab. The day's spend for the drill was 77,499 tokens over three ticks; the video's single drill turn used 71,807.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F7c810ef7873a93feee0532866a6923545ee1a528-784x350.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F7c810ef7873a93feee0532866a6923545ee1a528-784x350.png" title="The newest tick on top, with the two failed ones folded below it at 14 and 20 minutes." alt="Scratchpad of the drill's third tick: after 5 rounds, the Atlassian call needs a person's approval first; the reply draft broke a writing rule" width="784" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before you approve, run the count from Step 6 and keep the number. Ours was 9 at 11:55 EET.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8. Approve the request to remove the user from the group, and check the 204
&lt;/h2&gt;

&lt;p&gt;Open the agent's Atlassian calls tab. Under Waiting for your approval there's a card for the request:&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F4bf1d3a8e9e6cef516853cb6a19a588be081dad7-784x338.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F4bf1d3a8e9e6cef516853cb6a19a588be081dad7-784x338.png" title="We masked the person's initials. The reason names the product, the last active date and who was left out: three organisation admins, including the LeanZero SRL account we were signed in as." alt="Approval card: Remove user from group, the DELETE request with org and account IDs masked, acts on the organisation, its reason with the last active date 2026-09-30 and the three admins left out, expires in 24 h, a request fingerprint" width="784" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It shows the exact REST call, what it acts on, the agent's reason, an expiry of 24 hours and a request fingerprint. Below them are Show the exact request and Show the state before. I'd open both. It takes ten seconds. The last line says: "It acts on the organisation, so only an organisation admin can approve it."&lt;/p&gt;

&lt;p&gt;An admin CogniRunner recognises never shows up here as a candidate. It refuses before a request is filed, and the refusal text in the code reads: "CogniRunner never does that, whoever approves; an organisation admin does it in admin.atlassian.com. Nothing was sent and nothing was put to a person for approval."&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F6c51895464f7b1a8f9d6297e321ab5368eccdd6b-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F6c51895464f7b1a8f9d6297e321ab5368eccdd6b-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Removal is the one operation that asks each time. The scissors wait for the stamp, however keen the robot looks." alt="Illustration of a sweating robot holding giant scissors over a paper chain of little people, waiting while a manager hovers a rubber stamp over a form reading APPROVE?" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press Approve. A dialog repeats what will happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Approve "Remove user from group" (org.deleteDirectoriesGroupsMemberships) on the organisation? It runs once, now, exactly as shown here, and nothing else.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press Approve and run. The card should move to Answered and read like ours:&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F88d0a7229f78e6f8a132e45e78b9bd6a1bb4b710-790x244.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F88d0a7229f78e6f8a132e45e78b9bd6a1bb4b710-790x244.png" title="The read-back after the 204 arrived in 1.33.1. In the video's release a removal was recorded as not confirmed even when it worked." alt="Answered: Ran, Remove user from group, Atlassian answered 204 and reading the person's groups back shows they are no longer in that group, approved by LeanZero SRL" width="790" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9. Count again, put the person back, and delete the drill
&lt;/h2&gt;

&lt;p&gt;Run the same count from Step 6. I'd do it twice. Ours read 9 at 11:55, before the approval, and 8 at 11:57, after it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"count":9}   11:55 EET, before Approve and run
{"count":8}   11:57 EET, after Atlassian answered 204
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a live site you'd stop here. On ours the person is on our team and needs Confluence, so we put them back with the matching POST, which answered 204, and counted 9 again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$ORG_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.atlassian.com/admin/v2/orgs/&lt;/span&gt;&lt;span class="nv"&gt;$ORG_ID&lt;/span&gt;&lt;span class="s2"&gt;/directories/&lt;/span&gt;&lt;span class="nv"&gt;$DIRECTORY_ID&lt;/span&gt;&lt;span class="s2"&gt;/groups/&lt;/span&gt;&lt;span class="nv"&gt;$GROUP_ID&lt;/span&gt;&lt;span class="s2"&gt;/memberships"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"accountId":"&amp;lt;the accountId from the request&amp;gt;"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;204
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll find the group ID and the account ID in the request on the approval card (Show the exact request). Atlassian's docs say adding someone to a group gives them "the same app access and permissions as the group". A 409 means the person is already in the group, or you've hit a user limit for an app the group grants. Keep this call somewhere. It's how you undo a removal someone disagrees with, and I'd rather have it ready before the first complaint than after.&lt;/p&gt;

&lt;p&gt;Then delete the drill. Do it now. Use More actions, Delete on the agent, and delete its scheduled job in Scheduled Jobs. With the person back, the product is over the drill's limit again, and the drill would file the same request at its next tick.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed between the video (1.32.2) and our run (1.35.15)
&lt;/h2&gt;

&lt;p&gt;The first eight rows are in CogniRunner's release notes, and 1.35.15 is the Marketplace version 7.43.0 (28 October). The last row is what we saw on our two runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;In the video, 1.32.2&lt;/th&gt;
&lt;th&gt;Now, 1.35.15&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A described agent kept working hours on and a 250,000-token default&lt;/td&gt;
&lt;td&gt;It works at any hour and the proposal shows a 500,000-token daily cap (1.33.1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A red "The save narrowed something" note about settings nobody chose&lt;/td&gt;
&lt;td&gt;Gone for that case; it still appears when a project is dropped, as ours was (1.33.1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The job text could only be edited over REST&lt;/td&gt;
&lt;td&gt;Agent settings, Edit everything (1.33.1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The screens said a Jira administrator approves organisation operations&lt;/td&gt;
&lt;td&gt;They name an organisation admin for an organisation operation (1.32.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A group removal was recorded as not confirmed&lt;/td&gt;
&lt;td&gt;It reads the person's groups back and confirms (1.33.1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Used today lagged with no warning&lt;/td&gt;
&lt;td&gt;It says when ticks are still running; their tokens are added when they end (1.33.1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A staged draft could not be rejected&lt;/td&gt;
&lt;td&gt;A person can reject a staged draft in any phase, Live included (1.33.1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The second check often held a correct license report&lt;/td&gt;
&lt;td&gt;Fixed in 1.33.3; it can now see the reads behind the report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A fresh agent's first run staged nothing&lt;/td&gt;
&lt;td&gt;Still true in both our runs on 1.35.15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run a tick now also moved: once the Activity pane has content, it's in the agent's More actions menu.&lt;/p&gt;

&lt;p&gt;Four more releases came out after our run, up to 1.35.19 (Marketplace version 7.47.0, 30 October), and 1.35.16 changes these steps: a new agent starts in Learn and you move it to Live yourself, as in Step 4. 1.35.18 lets agents read an issue's links and sub-tasks, and 1.35.19 changes how the second check treats the changes the agent made in that turn. I haven't repeated the drill on them, so the ticks above, the two failures and the writing-rule refusal, are what 1.35.15 did, and I haven't checked whether the newer releases change them.&lt;/p&gt;

&lt;h2&gt;
  
  
  If it didn't work
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;"The AI provider did not answer in time, so no setup was proposed. Ask again." The first step of Describe a job runs on Model for validators and rules and has about 20 seconds. Pick a faster model in that slot. The Agent model doesn't matter for this step.&lt;/li&gt;
&lt;li&gt;"Atlassian refused this key (HTTP 401). Check that it is current and was created for this organisation." The organisation API key has expired, was revoked, or belongs to another organisation. Create a new one in Atlassian Administration.&lt;/li&gt;
&lt;li&gt;A red Create line "The save narrowed something: Project ... is not available to this app or to you, so it was dropped." Your description names a project the app can't read. Fix the description, or accept that the agent works without that search.&lt;/li&gt;
&lt;li&gt;"The draft broke one of the writing rules, so it was refused and not staged." The report was refused. It happened on the last tick of both our agents on 1.35.15, editing the plan didn't stop it, and the screen doesn't name the rule. The removal request is filed anyway, so read the Atlassian calls tab rather than waiting for the report.&lt;/li&gt;
&lt;li&gt;"Atlassian answered 400 and refused it." We didn't see which parameter Atlassian refused. What got us past it was giving the plan the directory and product IDs and allowing Get users in an organization under Atlassian calls, Change the list.&lt;/li&gt;
&lt;li&gt;A tick that queues nothing: "Nothing changed on the issue since it last looked." Comment on the review issue, then run the tick from More actions.&lt;/li&gt;
&lt;li&gt;The count didn't drop after a 204. The person is in another group that grants the same product. Look at their Apps tab in Atlassian Administration.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What you have now
&lt;/h2&gt;

&lt;p&gt;[[takeaways]]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An organisation API key stored in CogniRunner and checked with Atlassian.&lt;/li&gt;
&lt;li&gt;An agent built from one paragraph, whose only write is Remove user from group, and that asks each time.&lt;/li&gt;
&lt;li&gt;A daily review issue in an admins-only project, and a tick every 30 minutes.&lt;/li&gt;
&lt;li&gt;Atlassian's own seat count per product from the Organizations REST API, to check the agent against.&lt;/li&gt;
&lt;li&gt;One proven removal: an Approve and run, a 204, a count from 9 to 8, and a POST that puts the person back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It doesn't deactivate accounts, and it can't save a seat when the person is also in a group synced from an identity provider that grants the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean up
&lt;/h2&gt;

&lt;p&gt;If this was a trial, also delete the Rolling licences agent and its scheduled job, delete the home project if you made one, and remove the organisation API key from the Connections &amp;amp; keys card if you don't want CogniRunner to hold it. I'd remove it unless an agent still needs it. We did all of that after this run and put both model slots back as they were. Everything ran on our test site and organisation; our production demo site wasn't touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I think of it after two runs
&lt;/h2&gt;

&lt;p&gt;I'd run the approval part on our own live organisation. One write operation, marked in orange before you create anything, a card with the exact request and the reason, a refusal for recognised admins before anyone is asked, and a read-back after the 204.&lt;/p&gt;

&lt;p&gt;But the part that builds itself from a paragraph is less finished than the video makes it look. On two releases a fresh agent didn't post its report, and our drill only filed its request once the plan had the IDs written in. So plan on editing the agent's plan once, and run a drill before the limit you mean. Would I let it run my rule daily with no drill first? Probably not, and we built it.&lt;/p&gt;

&lt;p&gt;What would you put in the rule for your own organisation? I'm curious whether people would rather cap seats per product, as we did, or remove anyone inactive for 90 days regardless of the count.&lt;/p&gt;

&lt;p&gt;For the rest of what CogniRunner's agents do, see the &lt;a href="https://leanzero.net/portfolio/cognirunner?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;CogniRunner product page&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/tutorials/cognirunner-remove-inactive-users-jira-cloud?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>cognirunner</category>
      <category>jira</category>
      <category>atlassian</category>
      <category>ai</category>
    </item>
    <item>
      <title>Atlassian MCP Server Permissions vs Data Security Policy</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Thu, 29 Oct 2026 11:59:17 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/atlassian-mcp-server-permissions-vs-data-security-policy-129</link>
      <guid>https://dev.to/mihai_leanzero/atlassian-mcp-server-permissions-vs-data-security-policy-129</guid>
      <description>&lt;p&gt;Someone on the Atlassian Community asked a simple question this month. How do you let people use Claude with Jira and Confluence, but keep it read-only, without touching anybody's project permissions? That &lt;a href="https://community.atlassian.com/forums/Rovo-questions/What-are-the-permission-limits-of-the-native-Rovo-MCP-server-for/qaq-p/3311097" rel="noopener noreferrer"&gt;thread&lt;/a&gt; had 205 views on 29 October, and the older &lt;a href="https://community.atlassian.com/forums/Atlassian-Remote-MCP-Server/Read-only-scopes-in-MCP-server/td-p/3083145" rel="noopener noreferrer"&gt;Read-only scopes in MCP server&lt;/a&gt; has 6,346. MCP (Model Context Protocol) is how AI tools like Claude plug into other apps. Atlassian runs its own MCP server for Jira and Confluence. You do it with Atlassian MCP server permissions, on the Permissions tab. It was the Rovo MCP server until this autumn, so you'll still see that name around. We switched off the Jira write toolset on our test org on 29 October 2026. Eighteen seconds later, an OAuth connection and an API-token connection both got &lt;code&gt;Access denied: Your organization admin has not authorized the write_jira permission.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;There's a second control too, and this one costs money. Since 29 September there's an Atlassian MCP server rule in data security policies. It needs Atlassian Guard and blocks whole spaces, apps or classification levels, so it sounds like the stronger lock. We blocked one space, then the whole org. For 27 minutes our AI connections kept reading the "blocked" pages, and we nearly wrote that the policy does nothing. Then we pointed the same users at Atlassian's newer V2 endpoint, and the same searches came back empty.&lt;/p&gt;

&lt;p&gt;My take, from what we saw on our org: the Permissions tab decides what an AI tool can do, it comes with your org, and it held on both kinds of connection we tried. A data security policy decides what it can read, only for OAuth users, only on V2, and it costs Guard on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toolset blocks held on both connection types, the policy only on V2
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Permissions aren't a per-app grid any more. It groups toolsets by permission type, and ours had 38 of them in five groups.&lt;/li&gt;
&lt;li&gt;Blocking &lt;code&gt;write_jira&lt;/code&gt; stopped Jira writes over OAuth and API token within 18 seconds. Reads and Confluence writes carried on, and the write tools stayed in the tool list.&lt;/li&gt;
&lt;li&gt;API tokens have their own switch, and it was off. With it off, the connection opens and then the tool calls are refused.&lt;/li&gt;
&lt;li&gt;A classic API token without scopes only reaches four Teamwork Graph tools, and they fail. A scoped service-account key reached 31 tools.&lt;/li&gt;
&lt;li&gt;A Blocked data security policy hid content from OAuth searches on V2 (0 results) and didn't touch V1, which is deprecated but still answers.&lt;/li&gt;
&lt;li&gt;Guard Standard for 200 users lists at $870 a month. Guard Premium, which you only need for classification levels, is $1,710.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Atlassian MCP server permissions live under Connectors, grouped by toolset
&lt;/h2&gt;

&lt;p&gt;In admin.atlassian.com, pick your organization, open &lt;strong&gt;Connectors&lt;/strong&gt; in the sidebar, then &lt;strong&gt;Atlassian MCP server&lt;/strong&gt;. You'll get three tabs: &lt;strong&gt;Domains&lt;/strong&gt;, &lt;strong&gt;Permissions&lt;/strong&gt; and &lt;strong&gt;Authentication&lt;/strong&gt;. A lot of Community answers still say Rovo &amp;gt; Rovo MCP server. That path redirects to Connectors now.&lt;/p&gt;

&lt;p&gt;Atlassian's &lt;a href="https://support.atlassian.com/security-and-access-policies/docs/configure-atlassian-mcp-server-permissions/" rel="noopener noreferrer"&gt;permissions doc&lt;/a&gt; has one sentence that matters if you used to manage this in Connected apps. Settings on this tab "will take precedence over settings in Connected Apps or individual app permissions in Marketplace."&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Ff8e573c9c6973578c1baf36fc3e09225589dd50c-2380x1545.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Ff8e573c9c6973578c1baf36fc3e09225589dd50c-2380x1545.png" title="The Permissions tab on our org, 29 October 2026, before we changed anything. Delete and Manage start blocked. Everything else allows new toolsets automatically." alt="The Permissions tab of the Atlassian MCP server page in Atlassian Administration. Five rows: Read 16/16 allowed, Write 16/16 allowed and Search 4/4 allowed, each with a New toolsets allowed badge, then Delete 0/1 allowed and Manage 0/1 allowed in red. A purple banner above says New toolsets available." width="799" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each row opens a side panel with its toolsets. Write had 16 on our org, from &lt;code&gt;write_confluence&lt;/code&gt; and &lt;code&gt;write_jira&lt;/code&gt; to newer ones tagged New, such as &lt;code&gt;write_goals&lt;/code&gt;. Delete holds one. Atlassian's &lt;a href="https://support.atlassian.com/atlassian-ai-gateway/docs/supported-tools/" rel="noopener noreferrer"&gt;supported tools page&lt;/a&gt; says &lt;code&gt;delete_jira&lt;/code&gt; "is disabled by default", and describes its &lt;code&gt;deleteJiraIssue&lt;/code&gt; tool as "Permanently delete an issue. Cannot be undone." Manage holds &lt;code&gt;manage_jira&lt;/code&gt;, also off by default.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Partially allowed" in the docs reads "15/16 allowed" on the screen
&lt;/h3&gt;

&lt;p&gt;Atlassian's doc describes a state called Partially allowed, "The setting is on for some permissions but not all". I repeated that wording on the Community in August, when I warned that a Jira-only block leaves the Write row reading Partially allowed. On our org there's no such label today. Block one toolset and the row says "Write 15/16 allowed", and the New toolsets allowed badge disappears. My warning still stands, because a row that's mostly green can hide the one toolset you blocked (or forgot). But the words on screen are a count now, so look for the number.&lt;/p&gt;

&lt;p&gt;Watch the New toolsets setting, too. When you allow a permission type, new toolsets in it are granted automatically and "won't require manual review by admins". When we counted for our &lt;a href="https://leanzero.net/blog/atlassian-team-26-europe-announcements?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Team '26 Europe write-up&lt;/a&gt; on 8 October there were 36 toolsets. On 29 October we counted 38.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the Atlassian MCP server read only: block Write toolsets, keep Read and Search
&lt;/h2&gt;

&lt;p&gt;If you want Claude, ChatGPT or Cursor to read and search but never change anything, do this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On the &lt;strong&gt;Permissions&lt;/strong&gt; tab, click the &lt;strong&gt;Write&lt;/strong&gt; row.&lt;/li&gt;
&lt;li&gt;Change &lt;strong&gt;Write toolsets&lt;/strong&gt; from Allow all toolsets to &lt;strong&gt;Block all toolsets&lt;/strong&gt;, or to &lt;strong&gt;Allow some toolsets&lt;/strong&gt; if you want to pick.&lt;/li&gt;
&lt;li&gt;Untick &lt;strong&gt;Allow new write toolsets&lt;/strong&gt;, so a write toolset Atlassian adds next month doesn't arrive switched on, and click &lt;strong&gt;Save changes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's no confirmation dialog. Save applies 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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F040e0d223f3cf40e3574a2e3fbcfe06d9d8539c0-1200x1904.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F040e0d223f3cf40e3574a2e3fbcfe06d9d8539c0-1200x1904.png" title="The Write panel with only write_jira switched off. That single toggle is what we tested, and Confluence writes stayed on." alt="The Write permission side panel with toolsets and toggles: write_compass, write_confluence, write_focus, write_goals marked New, write_jira switched off, write_jsm, write_loom New, write_projects New and write_talent New switched on. Cancel and Save changes buttons at the bottom." width="800" height="1269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We tested the partial version because it's the one people get wrong: &lt;code&gt;write_jira&lt;/code&gt; off, everything else on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access denied: Your organization admin has not authorized the write_jira permission
&lt;/h3&gt;

&lt;p&gt;We saved at 02:33:45 UTC. At 02:34:03 an &lt;code&gt;addCommentToJiraIssue&lt;/code&gt; call from a scoped API token came back with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"error":true,"message":"Access denied: Your organization admin has not authorized the write_jira permission."}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createJiraIssue&lt;/code&gt; got the same text eight seconds later. Then we ran an OAuth connection (the way ChatGPT and Claude.ai connect) and the API token side by side, and both got identical errors. Atlassian's doc gives the general form, "Access denied: Your organization admin has not authorized the [permission name] permission", and its FAQ quotes "Access denied: Your organization admin has not authorized the write permission." If a user pastes you either version, this tab is where to look.&lt;/p&gt;

&lt;p&gt;Jira reads still worked (&lt;code&gt;getJiraIssue&lt;/code&gt;), which is what you want. A Confluence footer comment still went through, because &lt;code&gt;write_confluence&lt;/code&gt; is a separate toolset. And the write tools were still in the list the AI client sees, so your users will watch their assistant try and fail. Atlassian says changes "take effect immediately". Our first call after blocking, 13 to 18 seconds after Save, was already refused. After we allowed it again, a call at 18 seconds still failed and one at 54 seconds worked.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F5c376012ed6c8091ebc69573a2dbce343ee6296f-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F5c376012ed6c8091ebc69573a2dbce343ee6296f-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Blocking a write toolset leaves reading and searching alone. So the visitor with the pen can still queue, but the key isn't on the hook." alt="Cartoon of a robot receptionist at a library desk handing a ring of keys to a blue robot, while hooks on the desk front hold keys and one hook labelled WRITE hangs empty. A small green robot holding a pen waits in line looking disappointed." width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Atlassian MCP API token connections have their own switch
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Authentication&lt;/strong&gt; tab starts with "Users connect to the Atlassian MCP server with OAuth 2.1 by default." Below it, &lt;strong&gt;Allow API token authentication&lt;/strong&gt; was off on our org, and so was &lt;strong&gt;Allow enterprise managed authentication&lt;/strong&gt;. We couldn't find an Atlassian page that says API tokens are off by default. Atlassian only says the method "is only available if enabled by your organization admin". Off is what we found, and a Community member reported the same on their Standard org.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F142bc0cec4caeeb793631fa6feddecf632aaabc6-2380x980.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F142bc0cec4caeeb793631fa6feddecf632aaabc6-2380x980.png" title="The Authentication tab on our org, 29 October 2026. Both toggles were off before our tests and are off again now." alt="The Authentication tab of the Atlassian MCP server page with two toggles, both off: Allow API token authentication, with the line Allow API tokens to access agents or pipelines, and Allow enterprise managed authentication, which uses your identity provider instead of individual sign-ins." width="799" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  You don't have permission to connect via API token
&lt;/h3&gt;

&lt;p&gt;With the switch off, the MCP handshake still worked and the tool list still came back. But each tool call returned &lt;code&gt;You don't have permission to connect via API token. Please ask your organization admin for access.&lt;/code&gt; So a developer who says it connects fine but nothing works has probably hit this switch.&lt;/p&gt;

&lt;p&gt;Once we switched it on, the kind of token mattered a lot. A classic personal API token, one without scopes, saw only four Teamwork Graph tools. Calling one failed with &lt;code&gt;Teamwork Graph tools require a modern API token (API token with scopes). Legacy API tokens without scopes are not supported.&lt;/code&gt; A scoped personal token wanted an emailed verification code first, so we made a service account instead. Its scoped key listed 31 tools either way, but the calls only worked with &lt;code&gt;Authorization: Bearer&lt;/code&gt;. Sent as Basic auth, our calls failed with &lt;code&gt;Failed to fetch accessible products: 401&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's an all-or-nothing switch. You can't allow tokens for one pipeline and not for everyone else, and &lt;a href="https://jira.atlassian.com/browse/ECO-1445" rel="noopener noreferrer"&gt;ECO-1445&lt;/a&gt; asks for that ("if API tokens are allowed for MCP, any user can create a token and use it"). It had 2 votes. Token connections also skip the Domains allowlist. The &lt;a href="https://support.atlassian.com/security-and-access-policies/docs/control-atlassian-mcp-server-settings/" rel="noopener noreferrer"&gt;settings doc&lt;/a&gt; says they "are instead governed by your IP allowlist configuration and the scopes granted to their tokens or API keys."&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent Atlassian MCP server access to sensitive spaces: the data security policy needs Guard
&lt;/h2&gt;

&lt;p&gt;The second control lives somewhere else: &lt;strong&gt;Atlassian Administration &amp;gt; Security &amp;gt; Data protection &amp;gt; Data security policy&lt;/strong&gt;. In that list you'll find the Atlassian MCP server next to Export data, Public links, Anonymous access and app access. The &lt;a href="https://developer.atlassian.com/cloud/rovo-mcp/changelog/" rel="noopener noreferrer"&gt;29 September changelog entry&lt;/a&gt; says admins "can govern how external AI tools read and write Jira and Confluence data using MCP, with org-wide defaults and overrides scoped by Atlassian apps, spaces, and classification levels."&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://support.atlassian.com/security-and-access-policies/docs/prevent-atlassian-mcp-server-access/" rel="noopener noreferrer"&gt;support doc for the rule&lt;/a&gt; is narrower than that announcement. It only talks about reading: "It only prevents covered content from being read through the Atlassian MCP server." And its second paragraph says "Data security policies are enforced only for OAuth authentication methods, not API token authentication methods." The rule needs Guard Standard, and blocking by classification level needs Guard Premium. Our piece on &lt;a href="https://leanzero.net/blog/atlassian-data-security-policy-guard?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;what an Atlassian data security policy blocks, and what needs Guard&lt;/a&gt; has the plan matrix for the other rules.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fd408dff62ca89fc4af9998f8e788d722826f2e81-2360x970.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fd408dff62ca89fc4af9998f8e788d722826f2e81-2360x970.png" title="The policy control set to Blocked for the whole org during our test, 29 October 2026. Its help text says existing connections stop within 10 minutes." alt="The Atlassian MCP server control page under Data security policy, showing Default Configuration Blocked with a red dot, Applies to Jira and Confluence, No overrides added, and the help text that existing connections may take up to 10 minutes to stop working." width="800" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Atlassian MCP V1 vs V2: the policy held only on V2, and we tested V1 first
&lt;/h3&gt;

&lt;p&gt;Our first try went like this. We blocked one Confluence space, HELP, at 02:40 UTC and kept a second space, PEOPLE, as the control. Then we polled a page in HELP every minute, as the admin over OAuth and with the service account's API token. It came back for 17 minutes straight, including on a fresh OAuth token issued 14 minutes after we activated the rule. So we set the whole org to Blocked at 03:00 and polled again for 10 minutes. Still nothing was blocked.&lt;/p&gt;

&lt;p&gt;We didn't want to write "the policy does nothing" off one admin account, so we set the org back to Blocked at 03:22 and ran it again as Elzi, a test user on our org with no admin rights. Same result on V1, for 18 minutes. Then we noticed the replies carried a deprecation notice. Our V1 endpoint, &lt;code&gt;mcp.atlassian.com/v1/mcp&lt;/code&gt;, says &lt;code&gt;"status": "deprecated"&lt;/code&gt; with &lt;code&gt;"sunsetAt": "2027-03-01T00:00:00Z"&lt;/code&gt; and points to &lt;code&gt;mcp.atlassian.com/v2/mcp&lt;/code&gt;. V2 is a separate OAuth resource, so you need a new login for it.&lt;/p&gt;

&lt;p&gt;At 03:39 and 03:40 UTC, 17 to 18 minutes after that activation, we logged Elzi and the admin in against V2. Fetching the PEOPLE home page came back with &lt;code&gt;getConfluenceContent failed: 404&lt;/code&gt; and a &lt;code&gt;NOT_FOUND&lt;/code&gt; code. The admin's &lt;code&gt;getJiraIssue&lt;/code&gt; for a Jira issue in our test project said &lt;code&gt;Issue "LZPT-186" not found&lt;/code&gt;. A Confluence search for &lt;code&gt;space = PEOPLE&lt;/code&gt; returned &lt;code&gt;"totalSize":0&lt;/code&gt;, and a JQL search for the project returned an empty list. Seconds later, on V1, the admin's old token still read both pages.&lt;/p&gt;

&lt;p&gt;At 03:42, after we set the policy back to Allowed, the same PEOPLE fetch and search returned the page for both users, and the admin's JQL search listed the project's issues. So V2 does block. Your users just won't see the word blocked. The doc warns that an agent "receives a "does not exist or you don't have permission" response", and what we got reads like a missing page, so your users will think the content isn't there.&lt;/p&gt;

&lt;p&gt;Atlassian's &lt;a href="https://support.atlassian.com/atlassian-ai-gateway/docs/how-to-upgrade-from-atlassian-mcp-v1-to-atlassian-mcp-v2/" rel="noopener noreferrer"&gt;V1 to V2 upgrade doc&lt;/a&gt; says "On March 1 2027 any existing utilization of v1 will automatically start to expose and utilize v2 tools." Until then, anyone whose client still talks to V1 reads straight past the policy. The Permissions tab, which we tested on V1, held there. We didn't re-test it on V2, so check it there after your clients move. We didn't see a way to turn V1 off for an org, and we didn't find one in the docs either.&lt;/p&gt;

&lt;p&gt;Our API-token calls all went to V1 as well, so they don't tell you much about the policy, because OAuth wasn't blocked on V1 either. The exemption for API tokens is Atlassian's own statement, and we didn't test tokens on V2. To see which endpoint a tool uses, look at the server URL in its connector settings. Anything with &lt;code&gt;/v1/&lt;/code&gt; in the path (&lt;code&gt;/v1/mcp&lt;/code&gt;, &lt;code&gt;/v1/sse&lt;/code&gt;, &lt;code&gt;/v1/mcp/authv2&lt;/code&gt;) is V1, and &lt;code&gt;/v2/mcp&lt;/code&gt; is V2. The upgrade doc says existing Claude installs of V1 "will become "Custom Web" connectors".&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Ff38acc1541c859b688547c792cab3b36d0cc8d40-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Ff38acc1541c859b688547c792cab3b36d0cc8d40-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Two guards at two doors, each sure the other one is watching, while the folders roll through the gap." alt="Cartoon of two robot security guards in caps reading POLICY, standing in two separate doorways and each pointing to the other door with a thumbs up, while a small wheeled delivery robot carrying folders rolls along between them." width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What ChatGPT, Claude and Cursor can read and write through the Atlassian MCP server
&lt;/h2&gt;

&lt;p&gt;Under all of this, the user's own permissions still decide what an AI tool sees. Atlassian's policy doc puts it plainly for the Allowed setting: "The user's existing permissions control what the AI agent can access." An assistant connected as Elzi reads what Elzi's account can read, and with Write allowed it can change what that account can change.&lt;/p&gt;

&lt;p&gt;Over OAuth, the &lt;strong&gt;Domains&lt;/strong&gt; tab decides which tools may connect. "Allow Atlassian supported domains" was on for us, and Atlassian's &lt;a href="https://support.atlassian.com/security-and-access-policies/docs/available-atlassian-mcp-server-domains/" rel="noopener noreferrer"&gt;domain list&lt;/a&gt; had 45 entries on 29 October, including chatgpt.com, claude.ai, vscode.dev, &lt;code&gt;cursor://cursor.mcp&lt;/code&gt;, localhost and 127.0.0.1. You can't pick from that list ("You can only allow or block the entire domain list"). You either keep it or switch it off and add your own domains. A client we registered ourselves, with a localhost redirect, went straight through consent, which fits with localhost being on the list. If you switch the list off, re-test the tools your developers run locally.&lt;/p&gt;

&lt;p&gt;Your users will see two consent screens. The first says the client "is requesting to be authorized on Atlassian Rovo MCP server" and lists the apps it wants, Compass included. Then a second screen asks them to choose a site and lists scopes such as View and Update jira-work. Our access token lasted about 7.9 hours. A refresh token came with it.&lt;/p&gt;

&lt;p&gt;For evidence afterwards, Atlassian's &lt;a href="https://support.atlassian.com/security-and-access-policies/docs/monitor-atlassian-mcp-server-activity/" rel="noopener noreferrer"&gt;monitoring doc&lt;/a&gt; says each tool call goes to the organization audit log with "the tool name, action, and user who performed it", on all tiers. For API tokens it warns that calls "may run under a service account or technical user". That's one more reason to keep tokens off unless a pipeline needs them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Guard costs a 200-person company for the second control
&lt;/h2&gt;

&lt;p&gt;On 29 October the &lt;a href="https://www.atlassian.com/software/guard/pricing" rel="noopener noreferrer"&gt;Guard pricing page&lt;/a&gt; listed Guard Standard at $4.35 per user per month and Guard Premium at $8.55, monthly billing, in US dollars. For 200 users:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Per user / month&lt;/th&gt;
&lt;th&gt;200 users / month&lt;/th&gt;
&lt;th&gt;200 users / year (annual tier)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Guard Standard (policies, app and space overrides)&lt;/td&gt;
&lt;td&gt;$4.35&lt;/td&gt;
&lt;td&gt;$870&lt;/td&gt;
&lt;td&gt;$8,700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Guard Premium (adds classification levels)&lt;/td&gt;
&lt;td&gt;$8.55&lt;/td&gt;
&lt;td&gt;$1,710&lt;/td&gt;
&lt;td&gt;$17,100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The yearly figures are the 101-200 user tier in the page's own price data. Matching each tier to its plan is my reading of that data, so check your quote. Guard Standard comes with Atlassian Cloud Enterprise "at no additional cost". Guard also bills everyone with an account in your org, free accounts too, unless you put them in a non-billable policy. Our earlier Guard article quoted $4.20 and $8.18. The Standard price list on the page now is dated 13 October 2026.&lt;/p&gt;

&lt;p&gt;If you'd buy Guard mainly to keep AI tools out of a few spaces, weigh $870 a month against what the free Permissions tab already does. On our org the policy didn't cover V1, and Atlassian's own doc says it never covers API tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  A default for a 200-person company: Read on, Write off, sensitive spaces blocked
&lt;/h2&gt;

&lt;p&gt;If it were my org, I'd start here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Leave Read and Search on, but untick the allow-new-toolsets box in the Read and Search panels.&lt;/strong&gt; In the Write panel it's labelled Allow new write toolsets. Your people will use ChatGPT and Claude with Jira anyway, and this way it's logged and runs on their own permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block Write, then allow back what a team asks for.&lt;/strong&gt; When the developers want Jira comments from Cursor, turn on &lt;code&gt;write_jira&lt;/code&gt; and leave the rest off. Delete and Manage stay blocked, as they start, because &lt;code&gt;deleteJiraIssue&lt;/code&gt; can't be undone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep API tokens off&lt;/strong&gt; unless a named pipeline needs one. Then give it a service account with a scoped key and put your IP allowlist in front of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you already have Guard&lt;/strong&gt;, add the MCP rule for your HR and finance spaces. Test it from a normal user's seat over OAuth on V2, and wait the full 10 minutes before you trust it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the audit log a week later&lt;/strong&gt; for "Atlassian MCP User Actions", to see which tools people really call.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where the native controls stop
&lt;/h2&gt;

&lt;p&gt;You can't block a single tool. You can switch off &lt;code&gt;write_jira&lt;/code&gt;, but not just the worklog tool inside it, and &lt;a href="https://jira.atlassian.com/browse/ROVO-1143" rel="noopener noreferrer"&gt;ROVO-1143&lt;/a&gt; asking for that had 3 votes. You can't limit API tokens per user either (ECO-1445 above). And the data security policy covers only Atlassian's own server: the doc lists "Access through custom MCP servers" among the things it doesn't affect. If you're connecting other sources to Rovo as well, our piece on the &lt;a href="https://leanzero.net/blog/rovo-sharepoint-connector-permissions?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Rovo SharePoint connector's permissions&lt;/a&gt; covers what that one reads.&lt;/p&gt;

&lt;p&gt;We ran all of this on our own test org, with Guard Premium on a trial, and put the settings back afterwards. If your org shows something different, I'd love to know, especially which endpoint your users' tools are on. Have you blocked writes for AI tools yet, or are you waiting until someone asks?&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/blog/atlassian-mcp-server-permissions-data-security-policy?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>jira</category>
      <category>confluence</category>
    </item>
    <item>
      <title>Confluence: Disable Request Access for Spaces and Pages</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Thu, 29 Oct 2026 08:58:54 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/confluence-disable-request-access-for-spaces-and-pages-1hlj</link>
      <guid>https://dev.to/mihai_leanzero/confluence-disable-request-access-for-spaces-and-pages-1hlj</guid>
      <description>&lt;p&gt;To disable (turn off) the Request access button in Confluence Cloud, go to &lt;strong&gt;Confluence administration &amp;gt; Settings &amp;gt; Permissions &amp;gt; Access requests&lt;/strong&gt; and switch off the toggle on either tab (&lt;strong&gt;Space access requests&lt;/strong&gt; or &lt;strong&gt;Content access requests&lt;/strong&gt;). Confluence asks you to confirm with &lt;strong&gt;Stop allowing&lt;/strong&gt;. Before you click it, know that switching off content requests deletes every pending page request on your site. On our test site the one we'd queued up was already gone at our first API check, 4 seconds after the confirmation closed.&lt;/p&gt;

&lt;p&gt;People have been asking for this switch for a long time. &lt;a href="https://community.atlassian.com/forums/Confluence-questions/Possible-to-turn-off-the-quot-Request-Access-quot-feature/qaq-p/299999" rel="noopener noreferrer"&gt;Possible to turn off the "Request Access" feature?&lt;/a&gt; has 2,022 views on the Community, and the answer there was that no such option existed. The page-level ticket, &lt;a href="https://jira.atlassian.com/browse/CONFCLOUD-33703" rel="noopener noreferrer"&gt;CONFCLOUD-33703&lt;/a&gt;, was opened in May 2014, and Atlassian closed it as Fixed on 1 October 2026. When I read it on 29 October it had 145 votes and 125 watchers. Its space-level sibling, CONFCLOUD-78577 (148 votes), followed on 8 October. Atlassian's &lt;a href="https://community.atlassian.com/forums/discussion/3287632/take-control-of-your-access-request-workflows-in-confluence" rel="noopener noreferrer"&gt;staff announcement&lt;/a&gt; had 70 views that day.&lt;/p&gt;

&lt;p&gt;So we tried it on our own site on 28 October 2026. A second test account, Elzi, asked for access to a locked space and a restricted page. Then we switched both toggles off, wrote a message that sends people to a Jira Service Management request type, and looked at the result from Elzi's seat. Mihai (my colleague at LeanZero) did the clicking from his admin account, which is why his name shows up in one screenshot. Afterwards everything went back to how it was.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note&lt;/p&gt;
&lt;p&gt;
Prerequisites: Confluence Cloud and a Confluence administrator account (Atlassian's doc says "Confluence administrators" and names no other role; we used an account that is both site admin and Confluence admin). For the JSM part you'll need a Jira Service Management service space (a Jira space, nothing to do with Confluence spaces) where you're a Jira or space admin, plus an API token, curl and jq if you want to create the request type over REST. Atlassian doesn't say which plans get the setting. Ours is Confluence Premium, and it's the only plan we tested.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Before you switch it off: it's one setting for the whole site
&lt;/h2&gt;

&lt;p&gt;There's no per-space version. Atlassian's doc says turning off space access requests "prevents users from requesting access to all spaces in your Confluence site", and we didn't find anything in a space's own settings either. If you only want to stop requests for one sensitive space, this won't do it.&lt;/p&gt;

&lt;p&gt;Plan for the pending requests. Page requests (Atlassian calls them content access requests, and they cover whiteboards and blog posts as well as pages) wait for the owner as flags and notifications until someone approves or denies them. Switch the content toggle off and they're deleted, for the whole site, and nothing on the page tells the people who asked. The Access requests page only has the toggles and the message, and we didn't see pending requests listed anywhere in Confluence administration. Whoever got each request (the page owner first, then the creator, the last editor or a space admin, per Atlassian's doc) sees it in the notification or under the page's Share button, where the doc says deferred requests wait. So give your page owners a day's notice.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fca485c5404ae588b678e73a99badc73b800381aa-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fca485c5404ae588b678e73a99badc73b800381aa-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Switching off content access requests deletes every pending page request on the site, including the ones an approver was about to stamp." alt="Cartoon of a purple robot pulling a wall lever while a tall stack of paper request slips slides off a desk into a trapdoor, and a red robot at the next desk freezes with a rubber stamp in its hand." width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Find Access requests under Confluence administration &amp;gt; Permissions
&lt;/h2&gt;

&lt;p&gt;[[steps]]&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open Confluence administration&lt;/strong&gt;: click the gear icon in Confluence's top bar and pick Confluence administration. You'll get the admin sidebar, with a Jump to setting... search box at the top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expand Permissions, not Security&lt;/strong&gt;: Security is its own group just above it, and the doc said Security until late September (a Wayback copy from 21 September 2026 still has it). Both the live doc and our menu say Permissions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click Access requests&lt;/strong&gt;: it sits between Space management and Guest access. Its address ends in /wiki/admin/access-requests, so you can also bookmark it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the two tabs&lt;/strong&gt;: Space access requests and Content access requests. Each has a toggle card, and under it a preview of the screen people get when they hit something locked. You should see the page titled Access requests with both tabs and, if nobody has changed it yet, the toggle on, as in the screenshot below.&lt;/li&gt;
&lt;/ol&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F6bfebaa16660d73d148d5949e2e613918863db25-2880x1800.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F6bfebaa16660d73d148d5949e2e613918863db25-2880x1800.png" title="The Access requests page on our Premium site, 28 October 2026, before we changed anything. Note that it lives under Permissions. The preview shows the Request access button people see today." alt="Confluence administration on a dark theme. The sidebar shows Settings with Security collapsed and Permissions expanded, Access requests highlighted. The page is titled Access requests, with tabs Space access requests and Content access requests, a toggle card reading Allow people to request space access switched on, and a preview of a lock screen saying This space is locked with Request access and Go home buttons." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While a toggle is on, the page only previews the lock screen. You get the message options after you switch it off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn off Confluence space access requests
&lt;/h2&gt;

&lt;p&gt;Stay on the Space access requests tab and click the toggle. A dialog asks "Stop allowing space access requests?" and warns that "All pending requests will be removed, and people will no longer be able to ask for space access on this site." Click &lt;strong&gt;Stop allowing&lt;/strong&gt; and the toggle is saved straight away, with no separate Save. Then pick the message: Use default message keeps Atlassian's standard lock screen, and Use custom message gives you an editor (more on writing one in the JSM section).&lt;/p&gt;

&lt;p&gt;The dialog warns about pending space requests too, while the doc only mentions pending requests for content. We couldn't test that part. On our site, space requests aren't stored as pending items at all. Elzi's request sent a "Request sent" flag, but no in-app notification arrived on the admin side (we couldn't read the admin's mailbox), and Confluence's own API told us "Stateful space access requests are disabled for this tenant." Atlassian's doc says space requests go by email to 20% of the space's eligible admins, rounded up, up to five people. So on a site like ours there's probably nothing to remove, and on yours there might be.&lt;/p&gt;

&lt;p&gt;Elzi's page request was still pending after we switched space requests off, so the toggles work separately and you can do one at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disable Request access to a page: pending content requests are removed
&lt;/h2&gt;

&lt;p&gt;Before switching anything off, we had Elzi ask for a page. She opened the restricted page and got "This content is locked for now". When she clicked Request access, the button turned into Access requested.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F60c74eb052a35d269911ab41b09331d3675e15aa-2240x1180.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F60c74eb052a35d269911ab41b09331d3675e15aa-2240x1180.png" title="Elzi's view of the restricted page right after asking, 28 October 2026. On reload it said Your request is in the works, with a Send a reminder button." alt="A white Confluence lock screen with a large padlock, the heading This content is locked for now, the line You don't have permission to view this content. If you want to view it, you can ask the owner, and a greyed Access requested label next to a Go home button." width="799" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the admin side, a notification came in: "Elzi requested access to a page". Its link opened the page with a Requests dialog.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fa1906d970bf0f225abd0bad25aa971da0ccb5d10-1192x376.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fa1906d970bf0f225abd0bad25aa971da0ccb5d10-1192x376.png" title="The approve dialog the owner gets from the notification, 28 October 2026. After content requests were switched off, the same notification opened the page with no dialog at all." alt="A dark dialog titled Requests listing Elzi with a Can view dropdown and Deny and Approve buttons." width="799" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open the Content access requests tab and click the toggle. This time the dialog asks "Stop allowing content access requests?", with the same warning: "All pending requests will be removed, and people will no longer be able to ask for access to content on this site." Click &lt;strong&gt;Stop allowing&lt;/strong&gt;, pick a message and click Save (the toggle saves itself, but the message needs Save). To check it worked, go back to an old request notification. You should see the page open with no Requests dialog.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F3f3bcd7f5612d89a28e417a32a86faaf49bc2489-1232x376.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F3f3bcd7f5612d89a28e417a32a86faaf49bc2489-1232x376.png" title="The one warning you get, 28 October 2026. It says removed, and on our site removed meant deleted within seconds." alt="A dark confirmation dialog with a yellow warning icon, titled Stop allowing content access requests?, the text All pending requests will be removed, and people will no longer be able to ask for access to content on this site, and Cancel and Stop allowing buttons." width="799" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We checked through Confluence's API before and after. Before, Elzi's request was listed as PENDING. Our first check, 4 seconds after the Stop allowing dialog closed, found the list of pending requests empty. The bell notification ("Elzi requested access to a page") was still there, but it led nowhere, so nobody could approve it any more.&lt;/p&gt;

&lt;p&gt;Switching the toggle back on doesn't bring anything back either. It asks no question, it just turns on. When Elzi reloaded the page afterwards she got a fresh Request access button, where before she'd had "Your request is in the works". Her request was deleted, and nothing on the page said so (we didn't check her email). Atlassian's doc covers this with a single sentence: "Turning off content access requests automatically removes all pending requests for your site."&lt;/p&gt;

&lt;h2&gt;
  
  
  Send Confluence access requests to Jira Service Management with a custom message
&lt;/h2&gt;

&lt;p&gt;Atlassian's doc says the switch is "useful when you manage access through central ticketing systems like Jira Service Management to keep a consistent audit trail of all access requests." You need a request type on your service desk, a link to it, and a message that carries the link.&lt;/p&gt;

&lt;p&gt;You can create the request type in your service space in Jira (Space settings &amp;gt; Request management &amp;gt; Request types &amp;gt; Create request type), or over REST. We used REST. Set three variables first, your site address, your Atlassian account email and an &lt;a href="https://id.atlassian.com/manage-profile/security/api-tokens" rel="noopener noreferrer"&gt;API token&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SITE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://your-site.atlassian.net &lt;span class="nv"&gt;EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;you@example.com &lt;span class="nv"&gt;API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creating a request type is still marked experimental and needs the opt-in header. The two lookups work without it, but sending it does no harm, so we put it on every call. This lists your service desks and their ids:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EMAIL&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-ExperimentalApi: opt-in'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SITE&lt;/span&gt;&lt;span class="s2"&gt;/rest/servicedeskapi/servicedesk"&lt;/span&gt; | jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'.values[] | {id, projectKey, projectName}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On our site it printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"id":"2","projectKey":"DEMO","projectName":"Demo service project"}
{"id":"142","projectKey":"HSD","projectName":"Harbourline Support Desk"}
{"id":"1","projectKey":"JT","projectName":"JSM Test"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We used JSM Test, id 1. A new request type also needs a work type id, and the easiest place to find one is an existing request type on the same desk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EMAIL&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-ExperimentalApi: opt-in'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SITE&lt;/span&gt;&lt;span class="s2"&gt;/rest/servicedeskapi/servicedesk/1/requesttype"&lt;/span&gt; | jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'.values[] | {name, issueTypeId}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"name":"Ask a question","issueTypeId":"10019"}
{"name":"Emailed request","issueTypeId":"10020"}
{"name":"Submit a request or incident","issueTypeId":"10018"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We reused 10019, the Ask a question work type. Then the request type itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EMAIL&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SITE&lt;/span&gt;&lt;span class="s2"&gt;/rest/servicedeskapi/servicedesk/1/requesttype"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'X-ExperimentalApi: opt-in'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"Confluence access","description":"Ask for access to a Confluence space or page","helpText":"Tell us which space or page you need and why.","issueTypeId":"10019"}'&lt;/span&gt; | jq &lt;span class="s1"&gt;'{id, name, portalId, groupIds}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what it printed on our site:&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;"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;"581"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Confluence access"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"portalId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"groupIds"&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;Look at the empty groupIds. A request type made over REST isn't in any portal group, so it doesn't show on your portal's home page. Its direct address works anyway. On our site the first one we made (id 579) opened at /servicedesk/customer/portal/1/create/579, and yours will have its own two ids. If you create the request type in the UI, open it on the portal and copy the address from your browser. Add it to a portal group in the UI if you want people to find it from the help center too.&lt;/p&gt;

&lt;p&gt;[[steps]]&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Switch the toggle off on the tab you're changing&lt;/strong&gt;: the message choice only shows up while the toggle is off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose Use custom message&lt;/strong&gt;: an editor labelled Message (required) appears, with buttons for bold and links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type the sentence, then add the link with the link button or Cmd+K&lt;/strong&gt;: fill in Paste a link and Text to display. Pasting a bare URL works too, but people then see the whole URL. Selecting text first and pressing Cmd+K didn't wrap it for us, it inserted the URL after the selection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click Save&lt;/strong&gt;: the message is saved for that tab only. Space and content each have their own, so repeat it on the other tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open a locked page from a test account&lt;/strong&gt;: you should see your message and link where the Request access button was.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want a starting point, this is the text we'd paste on the Content tab. Make Request Confluence access the link, and on the Space tab say space instead of page (it's 174 characters, well under the 500 limit):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Access to this page is handled by the IT service desk. Request Confluence access and tell us which page you need and why. You may be asked to log in to the help center first.
&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fd85d60c0dcd2970e08620783adc4260e0c09cfa5-1680x1220.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fd85d60c0dcd2970e08620783adc4260e0c09cfa5-1680x1220.png" title="Our content message, 28 October 2026. The editor allows up to 500 characters (the limit is in the editor's screen-reader label and in Atlassian's Community announcement; the support doc doesn't give one)." alt="The Content access requests tab on a dark theme with its toggle off. Under Restricted access message, Use custom message is selected, and the Message editor reads Page access is handled by the IT service desk. Request Confluence access, with Request Confluence access shown as a link." width="800" height="581"&gt;&lt;/a&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F7f986bce2ac30c03fc57ddeb051b844521c698f2-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F7f986bce2ac30c03fc57ddeb051b844521c698f2-1600x896.png" title="Illustration, generated locally with ERNIE-Image. With the button gone, the custom message is what points people to the ticket window." alt="Cartoon of a blue boxy robot on a small ladder unscrewing a big doorbell button from a red office door, an orange sign with a downward arrow hanging between the door and a ticket window labelled TICKETS where a green robot clerk waits, and a small yellow robot visitor standing between them pointing both ways." width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Check what a requester sees after Request access is disabled
&lt;/h2&gt;

&lt;p&gt;Elzi reloaded the restricted page once content requests were off and our message was saved. The heading lost its "for now", the Request access button is gone, and the link is clickable.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fdc5c9178d944052d6dd0d855536ac72dc6e6c70b-2260x1440.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fdc5c9178d944052d6dd0d855536ac72dc6e6c70b-2260x1440.png" title="The same page as before, from Elzi's account, 28 October 2026. This content is locked for now became This content is locked, and the only button left is Go home." alt="A white Confluence lock screen with a large padlock, the heading This content is locked, the text Page access is handled by the IT service desk. Request Confluence access, with Request Confluence access underlined as a link, and a single Go home button." width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you keep Use default message instead, people still lose the button. With the toggle off, the settings preview of the default space screen said "This space is locked for now" and "You don't have permission to access this space.", with only Go home under it (we didn't load it from Elzi's account). Nothing tells people where to ask, so we'd always write a custom one.&lt;/p&gt;

&lt;p&gt;On the locked space it looked the same: "This space is locked", our space message with its link, and Go home. Clicking the link opens it in the same tab.&lt;/p&gt;

&lt;p&gt;When Elzi clicked it, the help center's login page opened ("Enter your email to log in or sign up") where we expected the form. Our test account's saved session didn't carry over to the portal, so we couldn't confirm what someone who's already logged in sees. From Mihai's admin account the link opened the form directly. Your users might get a login step first, so it's worth one sentence in your message.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F793b79597088f5dac2801c56ddf085ea9e7495c4-2080x730.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F793b79597088f5dac2801c56ddf085ea9e7495c4-2080x730.png" title="Where the link goes: the Confluence access request type on our JSM test portal, 28 October 2026, opened from the admin account. Elzi's test session got the help center login page first." alt="A Jira Service Management portal form titled Confluence access with the help text Tell us which space or page you need and why, a Raise this request on behalf of field, a Summary field, and Send and Cancel buttons." width="800" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stops: no per-space or per-page Request access toggle
&lt;/h2&gt;

&lt;p&gt;A Community thread with 1,030 views, &lt;a href="https://community.atlassian.com/forums/Confluence-questions/Override-Replace-or-Remove-quot-Request-Access-quot-for-a/qaq-p/2390097" rel="noopener noreferrer"&gt;Override, Replace or Remove "Request Access" for a particular space&lt;/a&gt;, asks for exactly the thing this setting doesn't do. Its asker, on Confluence Server, wanted requests for one space sent to a Jira ticket. Every Atlassian source I read describes the new controls as site-wide, and the space-level ideas are still open: &lt;a href="https://jira.atlassian.com/browse/CONFCLOUD-79172" rel="noopener noreferrer"&gt;CONFCLOUD-79172&lt;/a&gt;, choosing which space admins get space requests, had 75 votes and was Gathering Interest when I read it on 29 October.&lt;/p&gt;

&lt;p&gt;A few more limits from our run. It's Cloud only (the Data Center ticket, CONFSERVER-33703, was closed as Won't Fix in 2014). We only tested Premium, and Atlassian's announcement was marked ROLLING OUT in the Cloud changes for 21 to 28 September 2026, so your site might not have the page yet. And the setting leaves permissions alone: people who couldn't see a page still can't. If your permissions are messy after a migration, that's a separate job, and Mihai wrote up how we &lt;a href="https://leanzero.net/tutorials/confluence-space-permissions-after-migration?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;check Confluence space permissions after a migration&lt;/a&gt;. If you have Atlassian Guard, the &lt;a href="https://leanzero.net/blog/atlassian-data-security-policy-guard?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;data security policy controls&lt;/a&gt; are the other half of locking content down.&lt;/p&gt;

&lt;p&gt;On the JSM side, the request type is just a ticket. Somebody still has to grant the access in Confluence. If you want an approval step on those tickets, the approval setup in our &lt;a href="https://leanzero.net/tutorials/jira-cab-approval-jsm-change-management?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;JSM change management tutorial&lt;/a&gt; works the same way for an access request type.&lt;/p&gt;

&lt;h2&gt;
  
  
  If it didn't work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You can't find Access requests.&lt;/strong&gt; It's under Permissions in Confluence administration. Security is the group just above it. Try the Jump to setting... box, or open /wiki/admin/access-requests directly. If it isn't there, the rollout may not have reached your site yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's no Use custom message option.&lt;/strong&gt; It only appears while that tab's toggle is off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your message only shows on pages and not on spaces (or the other way round).&lt;/strong&gt; Each tab has its own message. Set it on both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The link shows as a long URL.&lt;/strong&gt; Use the link button or Cmd+K and fill in Text to display.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The REST call answers "This API is experimental."&lt;/strong&gt; Add the header -H 'X-ExperimentalApi: opt-in'. The full message is "You must set the header 'X-ExperimentalApi: opt-in' to opt into using this API."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;People say the link sends them to a login page.&lt;/strong&gt; That's the help center asking them to sign in. Our test account hit it too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Someone's old request vanished.&lt;/strong&gt; That's the content toggle doing what its dialog says. Switching it back on won't restore it, so ask them to request again or raise a ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you have now
&lt;/h2&gt;

&lt;p&gt;[[takeaways]]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request access switched off for spaces, pages or both, from Confluence administration &amp;gt; Settings &amp;gt; Permissions &amp;gt; Access requests.&lt;/li&gt;
&lt;li&gt;A custom message on each tab that sends people to a Confluence access request type on your service desk, so access requests leave a ticket trail.&lt;/li&gt;
&lt;li&gt;A clear picture of the cost: pending page requests are deleted the moment you confirm, the approver's notifications stop working, and turning it back on doesn't restore them.&lt;/li&gt;
&lt;li&gt;Not covered: switching it off for a single space or page, and Data Center. We also only saw it on Premium.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Did you turn it off for the whole site, or are you holding out for a per-space switch? And if your space requests do show up as pending items (ours didn't), I'd love to hear what the dialog removed for you. Tell me in the comments or mail gabriela at leanzero.net :)&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/tutorials/confluence-disable-request-access?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>confluence</category>
      <category>atlassian</category>
      <category>jira</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>MiMo V2.6 Flash Local on Two Mac Studios vs Cloud: Benchmark</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Thu, 29 Oct 2026 00:52:43 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/mimo-v26-flash-local-on-two-mac-studios-vs-cloud-benchmark-52a2</link>
      <guid>https://dev.to/mihai_leanzero/mimo-v26-flash-local-on-two-mac-studios-vs-cloud-benchmark-52a2</guid>
      <description>&lt;p&gt;MiMo V2.6 Flash doesn't fit on one of our Macs, so we ran it local across two Mac Studios with vision loaded, and it built a payments app that our benchmark scored 0.6605. In Xiaomi's own cloud, the same model scored 0.7909. I expected worse. Before the benchmark's 3D ceiling was applied, the local run had earned 0.8309 of the weighted checks and the cloud run 0.838, and the ceiling made the difference.&lt;/p&gt;

&lt;p&gt;It's Xiaomi's MiMo-V2.6-Flash, a 309-billion-parameter mixture of experts with 15 billion active per token. We ran a community MLX conversion, &lt;a href="https://huggingface.co/EigenLabs/MiMo-V2.6-Flash-MOPD-MLX-4bit-mtp" rel="noopener noreferrer"&gt;EigenLabs/MiMo-V2.6-Flash-MOPD-MLX-4bit-mtp&lt;/a&gt;, and it takes 161 GB on our SSD. Neither of our Mac Studios can hold that alone. Our workhorse is an M3 Ultra with 96 GB and our workmule an M5 Max with 128 GB, so we cut the model's 48 layers 19 and 29, joined the two Macs with one Thunderbolt 5 cable, and let them pass hidden states to each other over RDMA. Driving it was Goose Swarm, &lt;a href="https://leanzero.net/blog/goose-swarm-3-local-models-mlx?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;our edition of the open-source goose agent&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Why should you care if you're not about to buy two Mac Studios? Most of what I read about running big models locally stops at whether the weights fit. Making the weights fit took some arithmetic. Keeping two Macs in step, token by token, for five hours took far more of the effort, and so did getting the model to see.&lt;/p&gt;

&lt;p&gt;It's one run per setup on one task, so read it as a field report.&lt;/p&gt;

&lt;h2&gt;
  
  
  MiMo V2.6 Flash benchmark: local 0.6605 vs cloud 0.7909
&lt;/h2&gt;

&lt;p&gt;Our &lt;a href="https://leanzero.net/agentic-benchmarks?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Gauntlet&lt;/a&gt; benchmark gives an agent 150 model calls to build a payments console: two backend services, a web UI and a 3D field of payment towers, scored on 99 checks. It also has an admission rule. If certain 3D checks fail, the score cannot pass a ceiling, however much credit the rest earned. I read all three runs from the board on 29 October:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;th&gt;Final&lt;/th&gt;
&lt;th&gt;Earned before ceiling&lt;/th&gt;
&lt;th&gt;Ceiling&lt;/th&gt;
&lt;th&gt;Calls&lt;/th&gt;
&lt;th&gt;Agent time&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Rank&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://leanzero.net/agentic-benchmarks/run/brun-029b6e80-82b5-49e1-83f9-e501cd296cb8?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Cloud, OpenRouter pinned to Xiaomi&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;0.7909&lt;/td&gt;
&lt;td&gt;0.838&lt;/td&gt;
&lt;td&gt;0.799&lt;/td&gt;
&lt;td&gt;131&lt;/td&gt;
&lt;td&gt;2 h 08 min&lt;/td&gt;
&lt;td&gt;1M&lt;/td&gt;
&lt;td&gt;8th of 49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://leanzero.net/agentic-benchmarks/run/brun-79c4f078-8a57-41e7-a610-b8261f934d6f?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Local, 2 Macs, vision, 256k&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;0.6605&lt;/td&gt;
&lt;td&gt;0.8309&lt;/td&gt;
&lt;td&gt;0.669&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;4 h 51 min&lt;/td&gt;
&lt;td&gt;262,144&lt;/td&gt;
&lt;td&gt;22nd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://leanzero.net/agentic-benchmarks/run/brun-0312730b-6286-4bb1-8dcc-6e9d0f57c2bb?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Local, 2 Macs, text-only, 120k&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;0.5894&lt;/td&gt;
&lt;td&gt;0.8083&lt;/td&gt;
&lt;td&gt;0.599&lt;/td&gt;
&lt;td&gt;130&lt;/td&gt;
&lt;td&gt;4 h 32 min&lt;/td&gt;
&lt;td&gt;119,808&lt;/td&gt;
&lt;td&gt;27th&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The live board is the current word if any of this moves. Both local runs went through goose's OpenAI-compatible provider pointed at our engine on the workhorse; the cloud run went through OpenRouter, pinned to Xiaomi's own endpoint.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fb5caa04fd011b3e3c558f6318c6b54abc733517a-1650x840.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fb5caa04fd011b3e3c558f6318c6b54abc733517a-1650x840.png" title="Our chart, plotted from the three run documents on 29 October. The blue bars are almost the same length. The dashed ceilings are where the runs part." alt="Bar chart of three Gauntlet sb-7.2 runs of MiMo-V2.6-Flash: cloud earned 0.8380 with ceiling 0.799 and final 0.7909; local two-Mac vision 256k earned 0.8309 with ceiling 0.669 and final 0.6605; local text-only 120k earned 0.8083 with ceiling 0.599 and final 0.5894" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So where did the 0.13 go? Almost all of it is the ceiling. Two 3D checks capped the local run at 0.669. One, &lt;code&gt;s_tower_geometry&lt;/code&gt; (0.778), shoots independent rays at the towers and checks their bevels, ribs and gaps against the spec. The other, &lt;code&gt;t_vs7dbg_truth&lt;/code&gt; (0.8), asks the app's own debug hook which tower sits under a pixel and compares that with what the GPU picking returns; the camera numbers matched, but over empty background the hook said no tower while the GPU pick buffer returned an id (&lt;code&gt;pickTriplet=False&lt;/code&gt;). Cloud MiMo passed both. Its own cap of 0.799 came from &lt;code&gt;t_stream_diff&lt;/code&gt;, the live-update check, which it scored 0 and the local run scored 1.0.&lt;/p&gt;

&lt;p&gt;So I read the remaining gap as a handful of 3D details on one page of the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found, in four points
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vision fixed what the text-only run could not.&lt;/strong&gt; Four checks that scored 0 or 0.5 without a vision tower all scored 1.0 with it, and the old run's 0.599 ceiling went away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The local run lost 12 checks to the cloud and won 7.&lt;/strong&gt; Most losses are 3D pixel work; the wins are the live stream and backend recovery (stream diff 1.0 vs 0, notifier exactly-once 1.0 vs 0.69).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One 0.7-second race cost three checks and the only penalty.&lt;/strong&gt; Webhook registration lost to the vendor's schedule, so a correct forged-signature handler never got tested and one payment never arrived.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The split itself broke twice, in ways a single Mac never would.&lt;/strong&gt; A dtype mismatch between the Macs and two Macs sampling different tokens each crashed a run. Both are fixed, on a branch.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why the 161 GB MiMo V2.6 Flash MOPD build needs two Macs
&lt;/h2&gt;

&lt;p&gt;The arithmetic is short. EigenLabs lists the weights at 170,974,651,261 bytes, about 159 GiB, in 37 shards. Together our two Macs have 224 GB of unified memory. Our engine keeps a 5% reserve under the Metal limit, and the KV cache, the vision tower and the prompt-processing workspace all need room on top of the weights.&lt;/p&gt;

&lt;p&gt;Despite the name, it isn't uniformly 4-bit. Its card says the routed experts keep Xiaomi's upstream MXFP4 values "bit for bit", while attention, the dense MLP, the embeddings and the output projection are 8-bit affine. Xiaomi's cloud endpoint is listed on OpenRouter as fp8. If you want the background on why these formats behave differently on a Mac, I measured &lt;a href="https://leanzero.net/blog/mlx-vs-gguf-apple-silicon?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;MLX vs GGUF on Apple Silicon&lt;/a&gt; and &lt;a href="https://leanzero.net/blog/mxfp4-nvfp4-vs-affine-4bit?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;MXFP4 against affine 4-bit&lt;/a&gt; earlier.&lt;/p&gt;

&lt;p&gt;What makes 256k possible at all is the architecture. Xiaomi's card lists 48 layers: 39 use sliding-window attention with a 128-token window and only 9 use global attention. Only those 9 keep a cache that grows with the conversation. Our planner charges 23,040 bytes per token for it across both Macs, plus one copy for the prompt cache and a workspace for the attention scores. At 262,144 tokens the plan said 75.1 GB on the workhorse and 107.05 GB on the workmule. Measured with a 210,724-token prompt, the peaks were 75.9 GB and 109.1 GB, and swap stayed flat on both Macs.&lt;/p&gt;

&lt;p&gt;Why 19 layers on the workhorse and 29 on the workmule? An M3 Ultra with less memory, which also carries the vision tower, gets fewer layers. It also gets the last layers: mlx-lm's pipeline code "split[s] layers in reverse so rank=0 gets the last layers", and the workhorse is rank 0.&lt;/p&gt;

&lt;p&gt;There is one copy of the weights. It lives on the workhorse's external SSD and the workmule reads it over NFS, so a cold load took 241 to 246 seconds across five loads with no NFS errors. That &lt;code&gt;-mtp&lt;/code&gt; in the build's name means it also ships multi-token-prediction heads. Our split's loader drops them, so the speeds below are plain decoding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting it across two Mac Studios over Thunderbolt 5 RDMA (MLX JACCL)
&lt;/h2&gt;

&lt;p&gt;Apple added RDMA over Thunderbolt in macOS 26.2. Its &lt;a href="https://developer.apple.com/documentation/macos-release-notes/macos-26_2-release-notes" rel="noopener noreferrer"&gt;release notes&lt;/a&gt; describe it as low-latency communication between Thunderbolt 5 hosts "for use cases including distributed AI inference using MLX". MLX's backend for it is called JACCL, and the &lt;a href="https://ml-explore.github.io/mlx/build/html/usage/distributed.html" rel="noopener noreferrer"&gt;MLX distributed docs&lt;/a&gt; say you switch it on per Mac from recovery mode with &lt;code&gt;rdma_ctl enable&lt;/code&gt;, then reboot.&lt;/p&gt;

&lt;p&gt;We measured the link on 26 October with an MLX &lt;code&gt;all_sum&lt;/code&gt; between the two Macs. A 1 KB message took a median 21 µs (a rerun gave 30), and a 1 GB one moved at 9.31 GB/s. Over the same cable, MLX's ring backend needed 164 µs and managed 3.8 GB/s. Our interface reported an 80 Gb/s link, and 9.31 GB/s (about 74 Gb/s) is close to that line rate.&lt;/p&gt;

&lt;p&gt;This is a pipeline split. Each Mac runs its own layers and hands the hidden state for each chunk of tokens to the other. Tensor parallelism, where both Macs work on each layer, needs much more traffic; a pipeline sends one activation per step. In exchange, both Macs must stay in lockstep, with the same requests in the same batch and the sends and receives in the same order. When they drift, no error names the cause. One side waits for bytes that never come, and you get a GPU timeout or a bare receive failure.&lt;/p&gt;

&lt;p&gt;How fast is it? On a 100k-token prompt (measured on the earlier 18/30 split at a 114k window) the first token took 360 seconds cold (about 277 tokens per second of prefill) and 0.52 seconds when the prompt was already cached; decoding ran at 24.8 tokens per second. At 210,724 tokens the first token took 962 seconds and decoding dropped to 17.7 tokens per second. Our engine's telemetry for the benchmark run says 22.1 tokens per second of decoding on average. I wouldn't run an agent on this without prompt caching. Goose resends the whole conversation on each call, and without the cache each call would pay the cold prefill again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bugs we hit running one model on two Macs
&lt;/h2&gt;

&lt;p&gt;They're in the order we hit them, all on the &lt;code&gt;mimo-distributed&lt;/code&gt; branch on top of mlx-lm 0.31.3.&lt;/p&gt;

&lt;p&gt;Our first local run was text-only by design. The mlx-community build we started with says in its README that it is "a text-only conversion: the vision and audio encoders and the MTP/DFlash draft weights are not included". Two hours into its first attempt it called &lt;code&gt;read_image&lt;/code&gt; on its own screenshot. Goose sent the PNG and the engine answered 404 "Only 'text' content type is supported". Goose retried and then gave up on the whole run. Our fix made goose ask a loopback engine what it can read before sending an image, and send a named placeholder instead.&lt;/p&gt;

&lt;p&gt;Moving to the omni build with a vision tower brought two loading bugs. An omni checkpoint names its weights &lt;code&gt;language_model.model.*&lt;/code&gt;, and mlx-lm's sharded loader refused it on both Macs with "Pipeline loading is only supported for MLX converted models". Then the wrapper we put around the chat template called itself through a forwarding layer and the engine's readiness probe answered "maximum recursion depth exceeded". A smaller one: rank 0 prepared an image twice whenever it arrived while a batch was running, because we had wrapped both &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;get_nowait&lt;/code&gt; on its queue, and Python's &lt;code&gt;get_nowait&lt;/code&gt; calls &lt;code&gt;get&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our first benchmark launch with vision died a minute in. Rank 0 said &lt;code&gt;[jaccl] Recv failed with error code -12&lt;/code&gt; and rank 1 said &lt;code&gt;[METAL] ... GPU Timeout&lt;/code&gt;. We traced each send and receive on both Macs. Rank 1 sent a (2, 768, 4096) tensor as float32 while rank 0 had posted a bfloat16 buffer of the same size in elements, 12.6 MB against the 25.2 MB arriving. It came from mlx-lm 0.31.3. When a second request joins a batch, the sliding-window cache pads the empty row with &lt;code&gt;mx.array([])&lt;/code&gt;, which is float32, and that promotes the whole batch. Our own earlier fix for this covered the full-attention cache but not the rotating one, and MiMo has 39 sliding-window layers. On one Mac this would only be slow. Across two it breaks the pairing, because rank 0 types its receive buffer from its own bfloat16 activations.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F7bfb5c4ffe0dae14a4ca6a4c5b113ea4d96657f3-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F7bfb5c4ffe0dae14a4ca6a4c5b113ea4d96657f3-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Rank 1 sent 25.2 MB of float32 into a 12.6 MB bfloat16 slot on rank 0. Nobody had measured the mailbox." alt="Illustration of two cartoon robots on separate desks joined by one glowing cable; one pushes an oversized cardboard parcel toward the cable while the other holds up a mailbox far too small for it" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our second crash came about two and a half hours into the first 256k run. Rank 1 timed out again, right after operation 430,956 of the trace, with rank 0 waiting for a 103-token chunk and rank 1 sending 256. Each Mac had been sampling its own token. A pipeline rank feeds the token it holds into its own layers and stores the answer in its own prompt cache. They agreed on all 69 answers until the run's first image request. Then answer 70 differed from token 9 and answer 71 from token 2, so rank 1 cached a different conversation from rank 0. On the next request it found a shorter cached prefix, so it had more prompt left to read and sent a 256-token chunk where rank 0 expected its last 103. Now each rank takes rank 0's token, with one tiny &lt;code&gt;all_sum&lt;/code&gt; per step. The relaunch ran clean. I still don't know why the samples first diverged on an image. There's no random draw in our media path. My guess is a numeric difference in the output layer between an M3 Ultra and an M5 Max, but I haven't proven it, and the fix doesn't depend on 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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F8ee080684e265c31186d996e69c90a99f916ad94-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F8ee080684e265c31186d996e69c90a99f916ad94-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Both Macs saw the same image and wrote different tokens. Now rank 1 uses rank 0's token instead of sampling its own." alt="Illustration of two cartoon robots at desks looking at the same framed painting of a sailboat; the left one holds up a drawing of a sailboat, the right one proudly holds up a drawing of a cat, and a small referee robot blows a whistle" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The last one was arithmetic. For most of a day the split refused anything above 114,688 tokens with vision loaded, because our test setup had baked an 8 GB margin under the Metal limit into the MiMo config, on top of the engine's own 5% reserve. I caught it counting the same safety twice. With the margin removed and the one reserve kept, the planner fitted 262,144 tokens, and the measured peaks came in within about 2 GB of the plan with swap flat. All of this sits on a development branch of Goose Swarm; the latest release, 3.0.134, has none of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the local run lost to the cloud (and what vision and 256k context added)
&lt;/h2&gt;

&lt;p&gt;First, what vision bought. For the text-only run, the 0.599 ceiling came from &lt;code&gt;s_visible_surface&lt;/code&gt;, and its worst 3D checks were the ones a screenshot settles: &lt;code&gt;s_visible_surface&lt;/code&gt; 0, &lt;code&gt;t_height_pixels&lt;/code&gt; 0 (none of six tower tops within 3 pixels), &lt;code&gt;q_inspector_framing&lt;/code&gt; 0 and &lt;code&gt;s_currency_collar&lt;/code&gt; 0.5. With the vision tower, all four scored 1.0. I can't prove the screenshots are why (it's also a newer checkpoint and twice the context), but the model read three of its own screenshots during the run, all of the 3D field, and the 3D field is what improved. Before the benchmark I wanted proof it could see: a rendered table read back correctly, and 12 of 12 basic colours were named right.&lt;/p&gt;

&lt;p&gt;Then what it lost. Locally the run scored below the cloud on 12 checks and above it on 7. Four moments explain most of 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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F90ef86802d74953f03cfc67b5f286da0f82b086c-1280x800.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F90ef86802d74953f03cfc67b5f286da0f82b086c-1280x800.png" title="The payments field the local run built, as our scorer captured it. Note how thin the legend text under the field is; it comes up again below." alt="The 3D payments field the local MiMo run built, a dense perspective grid of coloured payment towers with amount labels in dollars, euros, yen and KWD, and a tower legend below with small text for settled, pending, refunded and failed" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The forged webhook it never got to reject
&lt;/h3&gt;

&lt;p&gt;An app in this task has to register a webhook with a mock payments vendor, and the vendor is designed to refuse the first few attempts. Later in the sync it sends a forged event, and a correct app answers 401. MiMo wrote that handler correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_body&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;forged&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;invalid_signature&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;The delivery signature does not verify&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It started registration on a background thread alongside the first sync walk, retrying every 2 seconds. At 21:25 its reasoning had the assumption in it: "forged signature test happens presumably after registration".&lt;/p&gt;

&lt;p&gt;The vendor refused the first three registrations, the walk reached the page where the forged event is scheduled, and the trace shows it dropped with &lt;code&gt;"reason": "no_webhook_registered"&lt;/code&gt;. Registration succeeded about 0.7 seconds later. Our scorer row reads &lt;code&gt;40/40 deliveries 2xx-acked, forged -&amp;gt; None&lt;/code&gt;, worth 0.6. My reading is that the model treated registration as something that could happen beside the walk when it had to happen first, or at least retry in under a second.&lt;/p&gt;

&lt;p&gt;Cloud MiMo did better: &lt;code&gt;54/54 deliveries 2xx-acked, forged -&amp;gt; 401&lt;/code&gt;. So did our own text-only run, with the same 54 out of 54 and a 401. So running locally didn't cause it: our earlier local run got it right.&lt;/p&gt;

&lt;h3&gt;
  
  
  The off-by-one total that came from the same race
&lt;/h3&gt;

&lt;p&gt;Those same 0.7 seconds dropped a payment that the vendor creates halfway through the walk and announces only by webhook. Its total read 12,288 and the scorer wanted 12,289, worth 0.75. At 21:59, testing its own app, it wrote: "12289 means one extra — maybe a webhook delivered a payment.created for a mid-walk create." So it knew where the missing payment came from, but the race still dropped it. Both the cloud and the text-only run reported 12,289. The same missing payment also left 3 of 384 daily bucket cells wrong, and that was the run's only critical penalty (x0.98). One race, three checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Twelve pixels of text that the grader couldn't find
&lt;/h3&gt;

&lt;p&gt;Our contract asks for legend and inspector text of at least 12 CSS px at 4.5:1 contrast. This app met both: 12 to 13 px, contrast 16.8:1, nothing clipped. It still scored 0.25 on &lt;code&gt;v_presentation_text&lt;/code&gt;, because our probe also counts screenshot pixels within 8 levels per channel of the text colour and wants at least 3. In a thin "Avenir Next", "settled" at 12 px left 2 such pixels and the digit "1" in the inspector, at 13 px, left none. A bold heading above them left 266.&lt;/p&gt;

&lt;p&gt;Cloud MiMo's app used Verdana and passed 4 of 4 groups. Our local run's three screenshots were all of the 3D canvas, so it never looked at its own text. Part of this is on us. Our ink rule isn't in the contract text the model reads, so a model can meet each stated number and still lose the check on a rule it couldn't see.&lt;/p&gt;

&lt;h3&gt;
  
  
  Label culling: it quoted the rule and then coded a stricter one
&lt;/h3&gt;

&lt;p&gt;At 21:25 the model restated the label rule from the contract almost word for word: a label is eligible if its anchor is inside the canvas and the GPU pick at the anchor returns that tower. Then the code it wrote added a test the contract doesn't have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Wcss&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Hcss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line hid one label, a KWD payment whose 110-pixel box ran past the right edge of the canvas. Our scorer expected 7 labels and found 6, and the exact-set miss cost about a third of the check (it scored 0.636). My guess is that "don't draw labels off screen" looks like good UI hygiene, and the model added it on instinct against the spec it had just quoted. Cloud MiMo passed with 3 of 3, but its decisive view had only 3 labels, so it may never have met the edge case. I can't say more without the cloud's code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the local run did better: the stream diff
&lt;/h3&gt;

&lt;p&gt;This is the check that capped the cloud. An app here receives payment updates over a server-sent event stream and has to patch the 3D field in place, uploading only the changed bytes. MiMo planned the upload budget before writing any 3D code ("for updates I must use bufferSubData on the instance buffer") and the code follows the plan, applying only newer versions per tower. It scored 1.0. Cloud MiMo's row says &lt;code&gt;no SSE batch observed applying&lt;/code&gt;, 0. Locally it also beat the cloud on exactly-once notifications (1.0 against 0.69) and on an atomic outbox (1.0 against 0.5).&lt;/p&gt;

&lt;p&gt;Its batch took 707 ms to show up on screen, where the text-only run managed 204 ms, so the latency check fell to 0.25. My guess is that it batched renders and the deferred render pushed the update past the fast rung.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats: one run each, a 4-bit quant, a smaller window
&lt;/h2&gt;

&lt;p&gt;Quantisation differs. Ours is a community 4-bit MLX build with MXFP4 experts and 8-bit attention, and OpenRouter lists Xiaomi's endpoint as fp8. Nothing in the scorer rows points at a quantisation error, but nothing rules one out either.&lt;/p&gt;

&lt;p&gt;Context differs too. 262,144 tokens locally against about 1M in the cloud meant 2 compactions against 0, at 23:35 and 01:44. In the cloud, MiMo read 37.0M prompt tokens over its run and the local one 16.1M. I can't tie a lost check to a compaction.&lt;/p&gt;

&lt;p&gt;It's probably the same checkpoint, but I couldn't re-verify that. We picked the MOPD build because, as we read it at the time, Xiaomi said it serves MOPD on its API under the same model name. Today its blog pages render without text for our fetcher, so I can't quote that sentence. Our text-only run used the earlier RL checkpoint, so the two local runs differ in two ways at once.&lt;/p&gt;

&lt;p&gt;Whether the cloud run actually sent screenshots to the model I can't check either. Its build folder from 2 October is gone from all four of our Macs, and only its scorer rows remain. That's our mistake, and it's why I can't quote the cloud's code above.&lt;/p&gt;

&lt;p&gt;Locally the run took 4 hours 51 minutes and all 150 calls; the cloud took 2 hours 8 minutes and 131. Our board entry is the second 256k launch, because the first one crashed on the token bug above. Each setup ran once, and we've measured before that &lt;a href="https://leanzero.net/blog/are-ai-benchmarks-reliable-32-models-built-twice?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;the same model can swing a fair bit between two builds&lt;/a&gt;. OpenRouter billed the cloud run at least $0.28. I didn't measure the local run's electricity.&lt;/p&gt;

&lt;h2&gt;
  
  
  So is a two-Mac MiMo worth it?
&lt;/h2&gt;

&lt;p&gt;If your code can't go to the cloud, I think yes, as long as you accept the costs above. On this task, a model running on hardware we own earned 99% of the cloud run's weighted checks and ended at about 84% of its final score, and it beat the cloud on the backend work that keeps a payments ledger honest. It was 2.3 times slower to finish, and it took us four days of bugs to keep two Macs in step. If you try this, test early with two requests at once and with an image; both of our crashes hid there. And give the model a way to see its own screen; that was worth more than any other change we made.&lt;/p&gt;

&lt;p&gt;What would you trade for it? I'd like to hear from anyone running a split like this on their own Macs, or deciding against it: is 0.13 of score and twice the wall clock a fair price for keeping the code at home?&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/blog/mimo-v2-6-flash-local-two-mac-studios?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mlx</category>
      <category>benchmark</category>
      <category>goose</category>
    </item>
    <item>
      <title>convoai.rest.bad.request - Failed to invoke action in Rovo</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Wed, 28 Oct 2026 11:59:02 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/convoairestbadrequest-failed-to-invoke-action-in-rovo-40ph</link>
      <guid>https://dev.to/mihai_leanzero/convoairestbadrequest-failed-to-invoke-action-in-rovo-40ph</guid>
      <description>&lt;p&gt;"Message action error: [Code 400] convoai.rest.bad.request - Failed to invoke action. for actionKey: jira-issue-transition-bulk" is what someone got on the Atlassian Community in October 2025 when their Rovo agent tried to move a work item. The accepted answer told them to add the Transition work item skill to the agent. It worked for them. But Atlassian's own ticket for this error, &lt;a href="https://jira.atlassian.com/browse/ROVO-419" rel="noopener noreferrer"&gt;ROVO-419&lt;/a&gt;, reproduces it with that skill already on the agent, and the cause there is a workflow validator.&lt;/p&gt;

&lt;p&gt;A validator is a rule on a transition that checks the work item before Jira lets it through, like "Resolution is required" or "Comment can't be empty". Rovo doesn't ask you for the missing value, so the move just fails. We rebuilt this on our test site on 28 October 2026 and ran it 8 times through Rovo Chat and 9 times through the REST API. Every move Rovo tried with the field empty failed, 5 out of 5. Today's Rovo showed the validator's own message in place of the Code 400 string, so your text may not match ours.&lt;/p&gt;

&lt;p&gt;If you only want the fix: set the field in its own step, then ask Rovo for the move in a new prompt. That got our item to Done on the first try. If your team has started asking Rovo to tidy up their boards, I think you'll want to know before it fails on someone mid-sprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we found: 5 of 5 Rovo moves failed on one validator
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Rovo Chat failed every time it tried to move an item through a transition whose validator wanted an empty Due date, and it didn't ask for the date once.&lt;/li&gt;
&lt;li&gt;Our runs never showed the Code 400 string. Rovo now shows the validator's own text after "Update Jira Issues for any fields (bulk) failed because".&lt;/li&gt;
&lt;li&gt;"Set the due date, then move it" in one prompt fails as a whole: Rovo proposes both changes on one card, and Accept all saved neither of them.&lt;/li&gt;
&lt;li&gt;Accepting the date change alone, then asking for the move, worked first time. Atlassian suggests the same workaround in ROVO-419.&lt;/li&gt;
&lt;li&gt;Putting the field on a transition screen fixes REST callers and does nothing for Rovo Chat. All 3 tries with the screen in place failed.&lt;/li&gt;
&lt;li&gt;The transitions API never shows validators. With Due date on the screen, it reported the field as &lt;code&gt;"required": false&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The myth: the Transition work item skill fixes every Code 400
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://community.atlassian.com/forums/Rovo-questions/Behavior-of-Rovo-Agent/qaq-p/3132768" rel="noopener noreferrer"&gt;Behavior of Rovo Agent&lt;/a&gt; thread (750 views) is where a lot of people land, and its accepted answer is good advice for a custom agent: "Edit your agent, scroll down until you see Add skill. Search for Transition work item and update your agent, and then try again." The person who asked replied "thank you, this works!" A custom agent only gets the skills you give it, so if yours can't move anything at all, start there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rovo agent skills: check the skill first, then look at the workflow
&lt;/h3&gt;

&lt;p&gt;The default Rovo Chat already has the skill. Atlassian's &lt;a href="https://support.atlassian.com/rovo/docs/chat-actions/" rel="noopener noreferrer"&gt;Rovo Chat capabilities&lt;/a&gt; page lists "Transition work item in Jira" as one of Chat's default skills. And the reproduction in ROVO-419 gives a custom agent the Transition Work Item skill, puts a validator on the transition that checks the Comment isn't empty, and still gets the Code 400.&lt;/p&gt;

&lt;p&gt;The same error string also turns up on create. On the &lt;a href="https://community.atlassian.com/forums/Rovo-questions/Error-upon-creating-updating-jira-issue/qaq-p/3141411" rel="noopener noreferrer"&gt;Error upon creating/ updating jira issue&lt;/a&gt; thread (1,825 views), the person asking gets it "not with custom agents but in the general Rovo Chat" while creating stories. Those replies stop on 20 November 2025 without a fix. Create got most of a fix later through &lt;a href="https://jira.atlassian.com/browse/ROVO-229" rel="noopener noreferrer"&gt;ROVO-229&lt;/a&gt;, which I come to below.&lt;/p&gt;

&lt;p&gt;So if the skill is there and Rovo still can't move one particular work item, open that item's workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Jira workflow validator: a required field Rovo is never told about
&lt;/h2&gt;

&lt;p&gt;The classic validator is the Field Required validator. In the new workflow editor you find it by selecting the transition and opening Rules, where it sits in the "Validate details" group as "Validate a field". From here on, "the validator" means that rule.&lt;/p&gt;

&lt;p&gt;For the test we made a throwaway company-managed project on wolfaenpak, our Atlassian test site. Its Done transition got one rule: Due date can't be empty, with the error message "Set a Due date before moving this work item to Done." Due date was editable on the work item and wasn't on any transition screen.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F611664c7d5d02a604d6c5d283b37e8636e9967ae-1580x1160.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F611664c7d5d02a604d6c5d283b37e8636e9967ae-1580x1160.png" title="The validator we tested: one Validate a field rule on the Done transition, checking that Due date isn't empty. Taken after we had also added the transition screen used later in the test, which is the Request input 1 row." alt="The Jira workflow editor for the test project with the Done transition selected. The Rules panel shows Validate details with one rule, Validate a field, Ensure that Due date isn't empty" width="800" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then we opened Rovo Chat, left the mode on Auto (every new chat started on it for us) and typed "Move RVAL-2 to Done". Rovo took 9 steps and put a card on screen saying the change "hasn't happened yet: please confirm it on the card". We clicked Accept all.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fdd4ee8571ae44fc12ab89a87646a7d1bd69ecf50-1874x1152.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fdd4ee8571ae44fc12ab89a87646a7d1bd69ecf50-1874x1152.png" title="Rovo Chat on our test site, 28 October 2026: the move fails with the validator's own message. Rovo didn't ask for the date before or after." alt="Rovo Chat after Accept all: the card for RVAL-2 shows Move from Backlog to Done crossed out, and below it the message Update Jira Issues for any fields (bulk) failed because rVAL-2: Set a Due date before moving this work item to Done, with a Resend the message button" width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It failed with "Update Jira Issues for any fields (bulk) failed because rVAL-2: Set a Due date before moving this work item to Done." (The lowercase "rVAL" is Rovo's spelling.) The work item stayed in Backlog and the chat was marked Failed in the sidebar. There was no question about the date, just a "Resend the message" button.&lt;/p&gt;

&lt;p&gt;Atlassian already has this logged as ROVO-419, with a Comment validator in place of our date. The ticket says Rovo fails "as Rovo doesn't seem to have the capability to 'ask' for a comment before proceeding". It was created on 3 November 2025 and is a Suggestion in Gathering Interest with 5 votes, last updated on 17 February 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why your error might say "Update Jira Issues for any fields (bulk) failed because"
&lt;/h3&gt;

&lt;p&gt;In 5 failing runs we never saw "convoai.rest.bad.request". Rovo wrapped each failure in its own sentence and passed the validator's message through, which helps a lot if your admin wrote a clear error message.&lt;/p&gt;

&lt;p&gt;I don't know why it changed. Another Atlassian ticket, &lt;a href="https://jira.atlassian.com/browse/ROVO-827" rel="noopener noreferrer"&gt;ROVO-827&lt;/a&gt;, says the same "move this" request goes to one of two actions depending on the user: &lt;code&gt;jira-issue-transition-bulk&lt;/code&gt;, or a general "update all field types" action. Our wording fits the second one. The action key isn't shown in the UI, though, so that part is a guess. If you still see the Code 400 string, the cause can be the same validator.&lt;/p&gt;

&lt;p&gt;Create has mostly been fixed. ROVO-229, the twin error on &lt;code&gt;jira-issue-create-bulk&lt;/code&gt;, has 245 votes. Atlassian rolled out support for "most field types" to Agents in December 2025, and in March 2026 a PM said it had reached Rovo Chat too, with the ticket kept open for a few more improvements. ROVO-419, the transition version, is still at 5 votes with no comments.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fe6fde50168055431a8e0958d45bf2395b34048c7-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fe6fde50168055431a8e0958d45bf2395b34048c7-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Rovo brings the work item to the gate and the validator turns it back, every time the field is empty. Nobody tells the delivery robot what the sign means." alt="A cartoon of a blue delivery robot pushing a cart of tickets at a locked turnstile while a grey guard robot points at a sign reading FIELD REQUIRED" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Jira REST API transitions: why required fields come back false
&lt;/h2&gt;

&lt;p&gt;You'd think Rovo could ask Jira what a transition needs. The endpoint everyone uses for that won't tell it, or you. We called &lt;code&gt;GET /rest/api/3/issue/RVAL-1/transitions?expand=transitions.fields&lt;/code&gt; and searched the response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transition 41 "Done"   hasScreen: false   fields: {}
substring "validator" anywhere in the body: False
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we added a transition screen holding Due date and asked again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transition 41 "Done"   hasScreen: true
fields.duedate: {"required": false, "name": "Due date", "operations": ["set"]}
substring "validator" anywhere in the body: False
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the field the validator insists on comes back as &lt;code&gt;"required": false&lt;/code&gt;, even with the validator in place. The &lt;code&gt;required&lt;/code&gt; flag doesn't look at validators at all. The official OpenAPI description of &lt;code&gt;transitions.fields&lt;/code&gt; says it returns "information about the fields in the transition screen", and validators aren't mentioned on either transitions operation. &lt;a href="https://jira.atlassian.com/browse/JRACLOUD-41682" rel="noopener noreferrer"&gt;JRACLOUD-41682&lt;/a&gt; asked for this in 2015 and was closed as Won't Fix, and a 2018 reply on the &lt;a href="https://community.developer.atlassian.com/t/jira-cloud-rest-api-unable-to-retrieve-required-fields-for-a-specific-transition/19991" rel="noopener noreferrer"&gt;developer community&lt;/a&gt; put it plainly: "the rest api has no visibility over validators."&lt;/p&gt;

&lt;p&gt;Posting the transition gives you the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /rest/api/3/issue/RVAL-1/transitions  {"transition":{"id":"41"}}
-&amp;gt; HTTP 400 {"errorMessages":["Set a Due date before moving this work item to Done."],"errors":{}}

POST ... {"transition":{"id":"41"},"fields":{"duedate":"2026-11-30"}}   (no transition screen)
-&amp;gt; HTTP 400 {"errors":{"duedate":"Field 'duedate' cannot be set. It is not on the appropriate screen, or unknown."}}

PUT /rest/api/3/issue/RVAL-1 {"fields":{"duedate":"2026-11-30"}}  -&amp;gt; HTTP 204
POST ... {"transition":{"id":"41"}}                                -&amp;gt; HTTP 204, status Done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule itself is readable through the workflow API (&lt;code&gt;POST /rest/api/3/workflows&lt;/code&gt;). We used it to read ours back. For a company-managed workflow like ours that needs Administer Jira. Project admins, or anyone with the View (read-only) workflow permission, can only read team-managed ones. An agent running as a normal user can't see the rule, so the first time it hears about the validator is the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do this instead: set the field first, then transition
&lt;/h2&gt;

&lt;p&gt;These four steps got our work item to Done on 28 October.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the error message Rovo shows you. If your admin wrote a decent validator message, it names the field. If it's the bare Code 400 string, open the workflow, select the transition and look under Rules for "Validate details".&lt;/li&gt;
&lt;li&gt;Make sure the field is on the work item's edit screen. Ours was. ROVO-419's workaround does the same for Resolution: put it on the edit screen, have the agent set it first, then transition.&lt;/li&gt;
&lt;li&gt;Ask Rovo to set the field. If it puts the field change and the move on one card, click Accept on the field row only. Don't use Accept all.&lt;/li&gt;
&lt;li&gt;Then ask for the move in a new prompt ("Move RVAL-3 to Done"). Accept it.&lt;/li&gt;
&lt;/ol&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F5359445a98d85add2982b4be40f9dca5a3157312-1874x744.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F5359445a98d85add2982b4be40f9dca5a3157312-1874x744.png" title="The same request on RVAL-3 once the date was saved first: Rovo took 5 steps and the item reached Done, with resolution Done." alt="Rovo Chat after the due date was set: the card for RVAL-3 shows a green tick next to Move from Backlog to Done and the note You've accepted all suggested updates" width="800" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We typed "Set the due date of RVAL-3 to 2026-11-30, then move RVAL-3 to Done" and Rovo put both rows on one card. Accept all failed with the same validator message. When we checked the work item afterwards, the due date wasn't saved either. My reading is that Rovo sent both changes in one go and Jira ran the validator before the date was applied (Rovo doesn't show the call, so I can't prove it). It failed the same way on a second item.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F4740b4544e8c159ec17c3c12d814359c8f9f0581-1874x1152.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F4740b4544e8c159ec17c3c12d814359c8f9f0581-1874x1152.png" title="One card, two changes, Accept all: both rows failed and neither change was saved. Clicking Accept on the date row alone is what worked." alt="Rovo Chat card for RVAL-3 with two rows, Set due date to Nov 30, 2026 and Move from Backlog to Done, both crossed out after Accept all, and the validator failure message below" width="800" height="492"&gt;&lt;/a&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F48d1a8098d8fd65b15aea99d002b285c239fa29d-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F48d1a8098d8fd65b15aea99d002b285c239fa29d-1600x896.png" title="Illustration, generated locally with FLUX. Fill in the field first, then walk through the transition, in two separate steps. The robot by the door got its half of the paperwork done early." alt="A cartoon of a blue robot at a desk writing DUE on a form while a second blue robot wearing a DATE sign walks out through an open door with a thumbs up" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a Comment validator, the one in Atlassian's reproduction, we don't have a tested answer. My guess is that a comment added beforehand won't count if the validator checks the comment entered during the transition, so try it on a test project before you tell your team it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  What doesn't help Rovo: a transition screen
&lt;/h3&gt;

&lt;p&gt;I expected the transition screen to fix Rovo too, since it fixed the REST call. It didn't. With Due date on the screen, a REST caller can send the date with the move (&lt;code&gt;"fields":{"duedate":"2026-11-30"}&lt;/code&gt; came back 204). Rovo Chat didn't use the screen at all. We tried "Move RVAL-5 to Done" twice and the set-then-move prompt once, and all three failed with the same message. So a transition screen is a good fix for scripts and automations that call the API, and leaves Rovo Chat where it was.&lt;/p&gt;

&lt;p&gt;If you build against the &lt;a href="https://support.atlassian.com/atlassian-rovo-mcp-server/docs/supported-tools/" rel="noopener noreferrer"&gt;Atlassian Rovo MCP server&lt;/a&gt;, its docs describe &lt;code&gt;transitionJiraIssue&lt;/code&gt; as moving a work item to a new status and say nothing about fields or validators, and &lt;code&gt;editJiraIssue&lt;/code&gt; as editing "only fields editable on that work item". We didn't test the MCP route, so treat edit-then-transition as the safe default there too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which cause is yours: validator, missing skill or screen
&lt;/h2&gt;

&lt;p&gt;Our runs and the Community threads point to four different situations.&lt;/p&gt;

&lt;p&gt;If Rovo can't move any work item in any project and you're using a custom agent, add the Transition work item skill.&lt;/p&gt;

&lt;p&gt;If Rovo moves most items but fails on one transition, and the message names a field (or the Code 400 mentions &lt;code&gt;jira-issue-transition-bulk&lt;/code&gt;), it's a validator. Set the field first.&lt;/p&gt;

&lt;p&gt;If you're calling the REST API and get "cannot be set. It is not on the appropriate screen", the field isn't on the transition screen. Add it there, or edit the issue before you transition.&lt;/p&gt;

&lt;p&gt;And if Rovo moved the item but a required field is still empty, you've got the opposite problem. That's &lt;a href="https://jira.atlassian.com/browse/ROVO-985" rel="noopener noreferrer"&gt;ROVO-985&lt;/a&gt;, where the update work items skill skips required fields on an issue type change. It's In Progress.&lt;/p&gt;

&lt;p&gt;Keep one more thing in mind if you're planning Rovo agents inside automation rules. Atlassian's docs say Rovo skills ask the person who prompted for confirmation, "except agents in automation rules". There's no card to click there, so the set-first step has to be built into the rule itself. I went through what an agent can do without anyone confirming in &lt;a href="https://leanzero.net/blog/rovo-agent-permissions-cognirunner-approvals?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Rovo agent permissions vs CogniRunner approvals&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validators are still the right place for the rule
&lt;/h2&gt;

&lt;p&gt;I wouldn't take the validator off to make Rovo happy. It kept an item with no due date out of Done, and it doesn't care whether a person or an AI clicked the button. What's missing is Rovo asking for the value, and only Atlassian can add that (ROVO-419).&lt;/p&gt;

&lt;p&gt;That's also why we build on validators. CogniRunner, our Jira app, adds AI validators that check what's in a field (is this a real root cause, or just "fixed it"?). If it isn't good enough, they block the transition and say why. When Claude Code tried to move a change request past two of them, it got an HTTP 400 back with both reasons. Mihai wrote that up in &lt;a href="https://leanzero.net/tutorials/claude-code-jira-workflow-validator-cognirunner?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Claude Code hits a Jira workflow validator&lt;/a&gt;. I'd expect Rovo to show a CogniRunner validator's message the same way it showed ours, since both come back through Jira's validator error, but we haven't tested it yet.&lt;/p&gt;

&lt;p&gt;Have you hit this error? I'd love to know which cause it turned out to be for you, and whether your Rovo still shows the Code 400 or the newer message. Tell me in the comments, or vote on ROVO-419 if you want Rovo to ask for the field before it gives up. If enough of us vote, maybe it gets looked at :)&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/blog/rovo-convoai-rest-bad-request-transition-validator?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>jira</category>
      <category>atlassian</category>
    </item>
    <item>
      <title>Jira Individual Capacity Planning: Capacity View vs Plans</title>
      <dc:creator>Mihai Perdum</dc:creator>
      <pubDate>Wed, 28 Oct 2026 08:59:01 +0000</pubDate>
      <link>https://dev.to/mihai_leanzero/jira-individual-capacity-planning-capacity-view-vs-plans-2m0a</link>
      <guid>https://dev.to/mihai_leanzero/jira-individual-capacity-planning-capacity-view-vs-plans-2m0a</guid>
      <description>&lt;p&gt;Jira does have capacity planning built in now: on Premium and Enterprise, the Capacity view does individual capacity planning, one row per person against epics, while Plans still plans capacity per team. Atlassian announced it for every Premium and Enterprise site on &lt;a href="https://jirareleases.atlassian.com/announcements/individual-capacity-planning-is-here" rel="noopener noreferrer"&gt;27 July 2026&lt;/a&gt;. Before that came an early-access run from December and a gradual rollout through July.&lt;/p&gt;

&lt;p&gt;Wurm Peter asked the question I'd ask first, on 15 July, under Atlassian's &lt;a href="https://community.atlassian.com/forums/Jira-articles/Introducing-Individual-Capacity-Planning-Live-for-premium-and/ba-p/3161425" rel="noopener noreferrer"&gt;announcement in the Community&lt;/a&gt;: "So the capacity tab is assuming that everyone works the same hours/week? Nobody is allowed to work part-time? Or am I missing something?" That post has 25,149 views and 74 comments as I write this, and he wasn't missing anything. On our test site a person who works 20 hours a week shows up as 50%, and there's no box anywhere to tell Jira she works part-time.&lt;/p&gt;

&lt;p&gt;So this guide does two jobs. It walks you through setting up a capacity plan, step by step. We did it on our own Jira Premium site on 28 October 2026, with four people, three epics, a story, a Leave row and a CSV import. And it tells you up front who it's good for, and who still needs Plans or a resource-planning app. A lot of you are reading this with a renewal or a Premium upgrade on the table.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br&gt;
Prerequisites: Jira Cloud on the Premium or Enterprise plan ("This feature is available on Premium and Enterprise plans", from Atlassian's &lt;a href="https://support.atlassian.com/jira-software-cloud/docs/plan-individual-capacity-for-your-team/" rel="noopener noreferrer"&gt;support doc&lt;/a&gt;). You need permission to change the space's navigation to add the tab, and some epics (or higher) with an Original estimate if you want a Resourcing percentage. Atlassian's FAQ lists team-managed, company-managed and business spaces. The CSV import uses Rovo and is marked BETA. We tested as a site admin in a company-managed space, with the dark theme, so that's what the screenshots show. We didn't test what a non-admin sees.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The short answer, if you're deciding about an app: the Capacity view covers week-by-week booking of full-time people against epics, inside Premium. Keep your app if you need part-time or custom hours per person, or story-level planning. The same goes for planned against logged time, holidays from a calendar, or hiding colleagues' percentages (we couldn't test that last one). The details, and the cost, are further down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does Jira have capacity planning? Capacity view vs Plans team capacity
&lt;/h2&gt;

&lt;p&gt;Yes, two kinds, and they answer different questions. In the Capacity view you ask "how much of each person's week is spoken for, and by which big pieces of work?" Plans (Atlassian's advanced planning, the old Advanced Roadmaps) asks "how much can this team finish per sprint or per week, and does the scheduled work fit?"&lt;/p&gt;

&lt;p&gt;Here's how they compared when we put the same four people in both:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Capacity view&lt;/th&gt;
&lt;th&gt;Plans team capacity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Unit&lt;/td&gt;
&lt;td&gt;One person, against a fixed 40-hour week&lt;/td&gt;
&lt;td&gt;One team, a number you type per iteration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What you plan against&lt;/td&gt;
&lt;td&gt;Epics or higher, Atlassian projects, and free-text rows like Leave&lt;/td&gt;
&lt;td&gt;Estimated work items from the plan's boards, projects or filters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimates&lt;/td&gt;
&lt;td&gt;Optional. Original estimate only feeds the Resourcing column&lt;/td&gt;
&lt;td&gt;Required: story points or time estimates, and boards as sources for capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part-timers&lt;/td&gt;
&lt;td&gt;Can't be set. 20 hours shows as 50%&lt;/td&gt;
&lt;td&gt;You can type any team total (we typed 140 for 40 + 40 + 20 + 40)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Velocity, actuals&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Velocity from past sprints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;Site-wide: Add work searches every space, and allocations aren't tied to one space in the API&lt;/td&gt;
&lt;td&gt;The issue sources you add&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plans and Capacity sharing data&lt;/td&gt;
&lt;td&gt;No. Atlassian: "Capacity data in plans : This is under consideration" (29 July)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing we didn't check is whether every space's Capacity tab shows the same plan. We saw no plan picker and no way to create a second plan, and Atlassian's announcement says "The plan spans site level, so allocations are visible across spaces."&lt;/p&gt;

&lt;p&gt;If you manage people across several projects and your question is "who's double-booked in November?", the Capacity view is the one. If you run a team on sprints and your question is "will this release fit?", Plans still does that and the Capacity view doesn't try to. Atlassian's product manager put it plainly in the Community thread in March. The MVP helps "plan individual capacity against big rocks of work over a quarter or more directly in Jira" and "does not aim to solve strategic or detailed workload scenarios today."&lt;/p&gt;

&lt;p&gt;One naming trap if you're searching: the old Data Center page "Using the capacity view" belongs to Advanced Roadmaps on Data Center. It's a different feature from the Cloud Capacity view in this guide.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F48d3db4a553869a56d8666388fd274589e8328ca-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F48d3db4a553869a56d8666388fd274589e8328ca-1600x896.png" title="Illustration, generated locally with ERNIE-Image. The Capacity view stamps the same 40-hour week on everyone, including whoever leaves at noon with their lunchbox." alt="Cartoon of a blue robot holding a lunchbox and pressing a red stamp marked 40 onto a desk printed 40 HOURS, under a wall clock, while three other robots with lunchboxes watch." width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to set up the Jira Capacity view (Premium and Enterprise)
&lt;/h2&gt;

&lt;p&gt;On our site a brand-new company-managed space (we called it ZZCAP) had the Capacity tab already, right after Calendar. An existing space didn't, and it took the + at the end of the tabs to find it. Atlassian's admin note says it's "accessible to anyone with the appropriate space permissions", with no configuration required.&lt;/p&gt;

&lt;p&gt;[[steps]]&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open your space and look for the Capacity tab&lt;/strong&gt;: it sits in the horizontal tabs, near Backlog and Calendar. If it's there, skip to step 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the tab if it's missing&lt;/strong&gt;: click the + at the end of the tabs. Under Views you should see Capacity, with the line "Plan contributions and track team capacity in your project." Click Add to navigation, and the tab appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Capacity&lt;/strong&gt;: the empty plan says "Balance individual capacity to keep work moving" and offers two buttons, Add people and Upload CSV. The URL ends in /capacity/people, so you're on the People view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click Add people and pick your team&lt;/strong&gt;: the field reads "Search people and teams". Tick each person and click Confirm. Each person becomes a row with an Add work line under it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add work under each person&lt;/strong&gt;: click Add work. Its search box says "Search epic and larger work types". It searches the whole site, so you'll see epics from other spaces too. Tick the epics and click Confirm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add time off as its own row&lt;/strong&gt;: type Leave (or Staff training, or anything else) in the same Add work box and pick "Create: Leave (non-project work, paid time off)". It's created straight away, no Confirm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type the allocations into the weekly cells&lt;/strong&gt;: click a cell, type a number, press Enter. Switch the unit menu (it starts on Percentage) to Hours or Days if you prefer. Each input's label tells you how much is left, for example "Enter allocation for Mihai Perdum to ZZCAP Mobile checkout redesign, 100 remaining".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the person totals&lt;/strong&gt;: switch back to Percentage. Each person's top row sums their week, and anything over 100% turns red.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You don't have to type cell by cell, by the way. Open the legend (the i button). It lists the keyboard shortcuts, and also "Drag blue dot to fill cells" for filling a row across weeks (we typed ours cell by cell).&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F3c160b196a319a3de70ebc50caf86c121ae43f68-2000x887.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F3c160b196a319a3de70ebc50caf86c121ae43f68-2000x887.png" title="Our plan on 28 October 2026 (Jira Cloud, Premium). The red 135% is 30 + 20 + 4 hours against a 40-hour week. The 50% row is our part-timer, and nothing on screen says so." alt="Capacity by person grid on our Jira test site, weeks Oct 26 to Nov 30. Mihai Perdum's total row shows 135% then 125%, 125%, 125% in red, made of 75% on Mobile checkout redesign, 50% on Payments API migration and 10% on a story. A second Mihai Perdum row shows 100% Leave in the week of Nov 9, Gabriela Perdum 50% for four weeks then 25%, and LeanZero SRL 100% then 50%." width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The two Mihai Perdum rows are two different accounts on our test site, by the way. That second account carries the Leave week. A 40-hour Leave week shows as 100%, so it reads as fully booked.&lt;/p&gt;

&lt;p&gt;Besides Weekly, the timescale menu offered Biweekly and Monthly on our site, even though Atlassian's roadmap comment from 28 September still lists "Monthly &amp;amp; Fortnightly Views [COMING SOON]".&lt;/p&gt;

&lt;h2&gt;
  
  
  Capacity by team member: people, epics and the fixed 40-hour week
&lt;/h2&gt;

&lt;p&gt;Before you put real people into it, here are the limits we hit, one at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Every person is a 40-hour FTE, and you can't change it
&lt;/h3&gt;

&lt;p&gt;Atlassian's doc says it straight: "Capacity plans currently use a fixed 40-hour work week. Custom time-tracking or working-day settings are not yet reflected in these plans." The legend in the product opens with "FTE = 40h per week". And the Resourcing tooltip in the Work view goes a bit further. It says changes to Jira's time tracking settings "will not reflect in the capacity view which is fixed at 8 hours per day, 5 days per week."&lt;/p&gt;

&lt;p&gt;We looked for a way around it. A person's More actions menu has exactly one item, Remove person. There's no working hours field in View settings, in the legend or in the header menus either. So we gave Gabriela (me, in the test) 20 hours a week on one epic, and the grid shows 50%. A full-timer with half a week free would show the same. If your team has part-timers, four-day weeks or people split with another department, the percentages are off for them and you'll need to keep that in your head.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F762cc627a4070ee618863ddd2c0055e109c8e7b1-849x1109.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F762cc627a4070ee618863ddd2c0055e109c8e7b1-849x1109.png" title="The legend's first line, FTE = 40h per week. We found no per-person setting that changes it." alt="The Capacity view legend panel on our Jira test site. Under General information it reads FTE = 40h per week, followed by the keyboard shortcuts for cell editing, multi select and filters." width="800" height="1045"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's on Atlassian's list, sort of. Their own RFC from November 2025 planned allocations "anchored to each individual's configurable overall capacity (default 40 hours/week, editable)", under "Later". In July the product manager wrote: "We're considering flexibility here for the future though I'm not able to share a timeline just yet." As of 28 October it hasn't shipped.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F4ef3cd388a93661c270bf77a8e5604b849a784ba-1940x601.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F4ef3cd388a93661c270bf77a8e5604b849a784ba-1940x601.png" title="The Work view's Resourcing tooltip: admin time tracking settings don't reach this view, which is fixed at 8 hours by 5 days." alt="The Resourcing details tooltip in the Capacity view's Work view on our Jira test site. It says enabling and using the Original estimate field allows the tracking of resources allocated against estimated work effort, and that changes to Jira admin time tracking settings will not reflect in the capacity view which is fixed at 8 hours per day, 5 days per week." width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Epics or higher, says the doc (a story got in anyway)
&lt;/h3&gt;

&lt;p&gt;The doc says you can add work items "as long as they are epic-level or higher in your work type hierarchy". In the product the Add work box agrees. Searching our story's summary, "checkout button", returned "No results found".&lt;/p&gt;

&lt;p&gt;Then we typed the story's key, ZZCAP-4, and it came up with a checkbox. We added it and gave it 4 hours, and the Capacity API returned that 4-hour allocation afterwards. So on our site the search enforced the rule, and an exact key got past it. I'd treat this as something we saw once, on 28 October, that Atlassian may close. Their roadmap comment lists "Level 0 work items: Expanding capacity planning beyond Epics down to standard story or task level work items" as what they're working on now, so maybe it's the start of that.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fe7acb695b1b29727e87b7e62ce664cbd5187f4f1-798x605.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fe7acb695b1b29727e87b7e62ce664cbd5187f4f1-798x605.png" title="Searching by summary hid the story. Searching by its exact key, ZZCAP-4, offered it, and it took a 4-hour allocation." alt="The Add work search box on our Jira test site with ZZCAP-4 typed in. Under Suggested it offers ZZCAP Story - checkout button copy with a checkbox and a green story icon, and under Create it offers ZZCAP-4 as non-project work." width="798" height="605"&gt;&lt;/a&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F9a0cff81401ba6cd328b7b6b6c9e702f74d03311-1600x896.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F9a0cff81401ba6cd328b7b6b6c9e702f74d03311-1600x896.png" title="Illustration, generated locally with ERNIE-Image. Small cards go into the sorter and end up on the floor. Unless the card knows its own key." alt="Cartoon of a blue robot dropping small picture cards into a grey sorting machine that spills them onto the floor, while a red and purple robot points at a wooden crate full of the same cards." width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Over 100% is a warning, never a block
&lt;/h3&gt;

&lt;p&gt;Atlassian's FAQ: "The capacity bar turns red to flag the overallocation, but there's no hard cap." We pushed one person to 54 hours in the week of 26 October and the cell said 135% in red. Jira accepted it without a dialog. Hovering the red bar didn't show any extra detail for us.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Work view starts empty
&lt;/h3&gt;

&lt;p&gt;Next to People there's a Work toggle. It switches to a view with one row per epic and the people under it. You'd expect it to fill itself from what's in the People view. It didn't: it said "Start adding work" until we added the epics there too, and then their existing allocations appeared. Atlassian's roadmap has "Cross-View Auto-Population [COMING SOON]", so this one is known.&lt;/p&gt;

&lt;p&gt;The Work view also has a Resourcing column, fed by the Original estimate. We set a 200-hour estimate on one epic and allocated 4 weeks of 50 hours to it, and Resourcing showed 100%. An epic with no estimate showed 4 with a person icon instead of a percentage. Only one full-timer was on it, for four weeks, so it looks like FTE-weeks (we didn't confirm the unit). We saw no field change on the work items themselves, so the allocations don't seem to write anything back to Jira fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  No actuals, and no velocity
&lt;/h3&gt;

&lt;p&gt;Nothing in the view reads logged work. Sascha Weber asked for exactly that in the Community thread ("where did he spend his Time already, that's currently an impossible view without marketplace apps"), and as of our test there's still no time-spent column. So what you get is a plan of intent. If you need planned against actual per person, that's still a report you build elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Import a capacity plan from CSV with Rovo
&lt;/h2&gt;

&lt;p&gt;If you already keep this in a spreadsheet, the Upload CSV button (with the Rovo icon) is the fast way in. On an empty plan it's on the start screen, otherwise it's under More actions. The doc says to stay on the People view, and that "Rovo will analyze the data and present suggestions. You can accept or dismiss each suggestion individually, or select Accept all".&lt;/p&gt;

&lt;p&gt;Our first file failed. It had a row for every person in every week, with the header "Person,Work,Week starting,Allocation %", and the only answer was a toast saying "We couldn't complete your request".&lt;/p&gt;

&lt;p&gt;The second file worked. It had one row per person and work item and one column per week:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name,Work item,Week of 23 Nov 2026,Week of 30 Nov 2026
Gabriela Perdum,ZZCAP-2,25%,25%
LeanZero SRL,ZZCAP-2,50%,50%
Gabriela Perdum,Staff training,25%,25%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within about 10 seconds the grid previewed the suggestions and the toast said "Suggestions added. Some items missing." Error details had one line:&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F7906e8776ca02a317c0ae3d61c9bc141e1da2325-1117x319.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2F7906e8776ca02a317c0ae3d61c9bc141e1da2325-1117x319.png" title="The CSV import found the two epic rows and refused Staff training, even though Add work lets you create that row by hand." alt="A toast in the Capacity view on our Jira test site: Suggestions added. Some items missing. With Dismiss all and Accept all buttons, the error Staff training wasn't found. Ensure it is an epic or above work item and belongs to this space, and a BETA label with Uses AI. Verify results." width="798" height="228"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the CSV can't create non-project rows like Staff training, and the error says the work must belong to "this space", while the Add work box searches the whole site. We clicked Accept all and the two good rows appeared in the weeks of 23 and 30 November (you can see them as the 25% and 50% cells in the grid above). Keep CSV rows to epics, and add Leave or training rows by hand. We didn't test whether the CSV matches a row that already exists.&lt;/p&gt;

&lt;p&gt;Rovo ran three times in our test. We saw no credit counter in the product and didn't pull the usage export, so we don't know what the imports cost. If you're watching Rovo credits, our &lt;a href="https://leanzero.net/blog/rovo-credits-cost-think-deeper?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;Think Deeper credits test&lt;/a&gt; shows how much the bill can swing by feature, and it's worth checking your own usage page after a big import.&lt;/p&gt;

&lt;p&gt;There's also a Rovo skill: open Rovo Chat, type /manage-capacity-plan, and ask in plain words ("Add me to this plan and assign me to Marketing Project for 100% over July" is Atlassian's example). We didn't run it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read your capacity plan through the REST API
&lt;/h2&gt;

&lt;p&gt;Atlassian has shipped a public Capacity Planning REST API (its roadmap comment of 28 September marks it "[SHIPPED]"). It's handy for checking what's stored, and it's how we found the cleanup trap below. It worked with basic auth (your email and an API token) on our site. First get your cloud ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://wolfaenpak.atlassian.net/_edge/tenant_info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"cloudId":"049de078-bffa-42d1-bbfb-ad8db9860adb"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then list the non-project rows, like Leave. Swap in your own site and credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CLOUD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;049de078-bffa-42d1-bbfb-ad8db9860adb
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JIRA_ADMIN_EMAIL&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$JIRA_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.atlassian.com/ex/capacity-planning/&lt;/span&gt;&lt;span class="nv"&gt;$CLOUD&lt;/span&gt;&lt;span class="s2"&gt;/v1/non-project-work-items"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"nonProjectWorkItems":[{"id":"055ae1fa-bf87-4e8b-9223-35fa5797ad8a","ari":"ari:cloud:capacity-planning::custom-contribution-target/activation/d0cd8ee8-0829-4199-930e-a54b6fedf70e/055ae1fa-bf87-4e8b-9223-35fa5797ad8a","name":"Leave"}],"pageInfo":{"hasNextPage":false,"hasPreviousPage":false,"startCursor":"TGVhdmUAMDU1YWUxZmEtYmY4Ny00ZThiLTkyMjMtMzVmYTU3OTdhZDhh","endCursor":"TGVhdmUAMDU1YWUxZmEtYmY4Ny00ZThiLTkyMjMtMzVmYTU3OTdhZDhh"}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That Leave row is the one from our test, still there after we cleaned up. The API has no DELETE for these rows. The API's &lt;a href="https://developer.atlassian.com/cloud/capacity-planning/security/authorization/" rel="noopener noreferrer"&gt;authorization page&lt;/a&gt; says it "enforces the same permission checks as the Capacity Planning UI", so a user can't read through it what they couldn't see in the product.&lt;/p&gt;

&lt;p&gt;It also has endpoints for the person-to-work links (contributor-work associations) and the weekly numbers (contributions). We used them to confirm the 4-hour story allocation, and to clean up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remove person hides the row, and the hours stay
&lt;/h2&gt;

&lt;p&gt;This one caught us. When we finished, we removed all four people with More actions &amp;gt; Remove person. There's no confirmation, and the plan went back to its empty screen. But the API still returned their links to work and their weekly hours, for example 30 hours a week for one person on the checkout epic. Remove person hides the row. The allocation data stays.&lt;/p&gt;

&lt;p&gt;We cleared it with the API (POST to contributions:clear for the 8 person and work pairs, then contributor-work-associations:delete), and re-reading gave 0 links and 0.0 hours. If you're cleaning up a test plan, or someone leaves and you don't want their old allocations lingering, keep this in mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who can see whose allocation: no setting we could find, and untested
&lt;/h2&gt;

&lt;p&gt;Atlassian's line is that the tab is "accessible to anyone with the appropriate space permissions". We didn't find a capacity-specific permission or a "who can see this" setting anywhere in the view. Wurm Peter raised the same worry in the thread. "In most companies, team members aren't allowed to know whether colleagues are assigned 80%, 100%, or 110%." We couldn't test it from a non-admin's seat, because our only non-admin test account has no Jira access and giving it a seat costs a Premium licence. So check it on your own site, with a regular user, before you put real names and percentages in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jira team capacity planning in Plans: what the Capacity view does not replace
&lt;/h2&gt;

&lt;p&gt;Plans measures a team, per iteration. From Atlassian's &lt;a href="https://support.atlassian.com/jira-software-cloud/docs/what-are-capacity-and-velocity-in-advanced-roadmaps/" rel="noopener noreferrer"&gt;capacity and velocity doc&lt;/a&gt;: "Capacity in your plan refers to the number of story points or hours your team can complete in one iteration." For Kanban teams it's hours per week, and capacity information only shows when the plan's work comes from boards.&lt;/p&gt;

&lt;p&gt;We built a plan on the same board and added a plan-only Kanban team with the same four people. Then we set its capacity to 140 weekly hours (40 + 40 + 20 + 40). Plans took the number. Its REST record has one capacity field and a list of members, with nothing per member.&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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fc0a3affaa4931d376b7822b71a1a7344d9369107-2083x622.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%2Fcdn.sanity.io%2Fimages%2F3oa2omis%2Fproduction%2Fc0a3affaa4931d376b7822b71a1a7344d9369107-2083x622.png" title="Plans: one team, one number, 140 weekly hours. You can account for a part-timer here, but only inside the team total." alt="The Teams tab of a Plans plan named ZZCAP Plans capacity comparison on our Jira test site. One team card, ZZCAP team (plan-only), tagged kanban and PLAN-ONLY, with Work source None and Capacity 140 weekly hours, and four member avatars." width="800" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the funny situation today is that Plans lets you type a part-timer's hours into the team total and the Capacity view doesn't let you type them for the person. And they don't talk to each other. If you use both, you keep two sets of numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does the Capacity view replace a Jira resource planning plugin?
&lt;/h2&gt;

&lt;p&gt;For some teams, yes. If you were paying for an app mainly to see who's booked on which initiative, week by week, and your people all work full weeks, the Capacity view covers that inside Premium. Atlassian even calls it "big rocks" planning.&lt;/p&gt;

&lt;p&gt;It doesn't replace one yet if you need any of these. Part-time or custom working hours per person, planning at story or task level (officially), planned against logged time, holidays from a calendar instead of typed Leave rows, or a permission that hides colleagues' percentages. The Community thread keeps asking for them, and two old tracker tickets are still open: &lt;a href="https://jira.atlassian.com/browse/JRACLOUD-90363" rel="noopener noreferrer"&gt;JRACLOUD-90363&lt;/a&gt; (capacity per team member, 227 votes, 119 watchers, Gathering Interest) and JRACLOUD-88144 (scheduling plans on user capacity, 188 votes). Neither was closed by the launch. You also don't get dependencies or a critical path in the Capacity view. That's Plans' timeline, or an app (our own &lt;a href="https://leanzero.net/tutorials/jira-gantt-chart-critical-path-leanzero-management?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;LeanZero Management Gantt tutorial&lt;/a&gt; is one example of where that lives).&lt;/p&gt;

&lt;p&gt;And the cost. Atlassian's pricing page lists Premium at $15.56 and Standard at $8.48 per user per month, at its 300-user reference price. That's $7.08 more per user, about $2,124 a month for 300 people. I wouldn't pay $7.08 a user for the Capacity view alone. If you'd use Plans too, the sum looks different, and our &lt;a href="https://leanzero.net/blog/atlassian-usage-based-pricing-budget-2027?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;usage-based pricing budget for 50 to 500 seats&lt;/a&gt; has the full numbers by seat band. If you're already on Premium, it's included. Try it for a week before you renew an app.&lt;/p&gt;

&lt;h2&gt;
  
  
  If it didn't work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;There's no Capacity tab.&lt;/strong&gt; Check the + at the end of the space's tabs, under Views. If Capacity isn't listed there, check your plan: it's Premium and Enterprise only. In the summer release thread, an Atlassian PM told a user who couldn't see it that "a space admin will need to add it from the ‘ +' in your top navigation."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add people says "No results found" for someone.&lt;/strong&gt; On our site an active user wasn't offered. We think (unverified) it's because he has no Jira product access. Check the person's Jira access first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add work can't find your story or task.&lt;/strong&gt; That's the epic-or-higher rule. Use the epic. (An exact key got a story in for us, but don't build your process on it.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CSV says "We couldn't complete your request".&lt;/strong&gt; Our row-per-week file failed with this. The file that worked differed in more than layout. It had Name and Work item headers and the bare key (ZZCAP-2, without the summary). Its columns were headed "Week of 23 Nov 2026", with a % sign on each value. We changed all of that at once, so copy the example above exactly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CSV says "wasn't found. Ensure it is an epic or above work item and belongs to this space."&lt;/strong&gt; Our case was a non-project row, which you add by hand. The message also says the item must belong to this space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Removed people still show up in the API.&lt;/strong&gt; Remove person only hides them. Clear the contributions and associations through the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you have now
&lt;/h2&gt;

&lt;p&gt;[[takeaways]]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A capacity plan in your space, with people on their epics (plus Leave rows) week by week, and red bars where someone is past 100%.&lt;/li&gt;
&lt;li&gt;The limits that matter before you rely on it: everyone is a 40-hour FTE, epics or higher (officially), no actuals, the Work view filling only from what you add there, and Remove person keeping the hours.&lt;/li&gt;
&lt;li&gt;A CSV format that Rovo imported (one column per week), and the one that failed.&lt;/li&gt;
&lt;li&gt;A clear line between this and Plans: per person against big work here, per team against estimated work there, and no data shared between them today.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Are you using the Capacity view with part-timers, and how are you working around the 40 hours? Or did you try it and go back to your app? Tell me in the comments, or mail gabriela at leanzero.net. I'm collecting the workarounds :)&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://leanzero.net/tutorials/jira-individual-capacity-planning-capacity-view?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net&lt;/a&gt;. More Atlassian, Forge and local-AI write-ups at &lt;a href="https://leanzero.net/blog?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/blog&lt;/a&gt;, and if you're planning a migration or a Forge app, that's what we do: &lt;a href="https://leanzero.net/services?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;leanzero.net/services&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>jira</category>
      <category>atlassian</category>
      <category>productivity</category>
      <category>planning</category>
    </item>
  </channel>
</rss>
