The Teamwork Graph API will be made available through Forge's Early Access Program (EAP).
EAPs are offered to selected users for testing and feedback purposes. These features are unsupported and are subject to change without notice.
You must only install apps that call the Teamwork Graph API in test organizations. Apps calling
the Teamwork Graph API require the read:graph:jira scope, which provides access to Teamwork Graph
data across your entire organization. While apps still respect end-user permissions, this scope may
grant access to sensitive information. For safety, only install these apps in organizations with test
data. Do not install apps using this API in organizations with production data while this feature is in EAP.
Additionally, this EAP has significant limitations. To review the full list of limitations, see Limitations and considerations.
You must be part of this EAP in order to use the Teamwork Graph API. Express interest in joining through this form.
This tutorial shows you how to query data from Teamwork Graph using the GraphQL API in a Forge app. You'll learn how to use Cypher queries to traverse the graph and retrieve connected data.
In this tutorial, you'll learn how to:
To complete this tutorial, you'll need:
You should also be familiar with:
The Teamwork Graph API uses a two-layer query approach:
This pattern allows you to first find the objects you need using Cypher's powerful graph traversal, then use GraphQL to retrieve the specific fields you want from those objects.
If you haven't already, create a new Forge app following the Getting started with Forge guide. Once you have a Forge app set up, you'll need to configure it to access the Teamwork Graph API.
To access the Teamwork Graph API, add the appropriate scope to your app's manifest.yml file based
on which Atlassian app you are developing for:
For Jira apps:
1 2permissions: scopes: - 'read:graph:jira'
For Confluence apps:
1 2permissions: scopes: - 'read:graph:confluence'
The read:graph:jira and read:graph:confluence scopes grant your app read access to Teamwork Graph data across the entire organization, including objects (work items, documents, projects, teams, users), relationships, and metadata. While apps respect end-user permissions, these scopes provide organization-wide access. This is why apps using these scopes should only be installed in test organizations during the EAP. See limitations and considerations for more details.
Without the appropriate scope for your app, your forge deploy command will fail when making Teamwork Graph API calls.
Before building complex queries, verify your app can successfully connect to the Teamwork Graph API.
The simplest way to test connectivity is with the built-in echo query:
1 2import api from '@forge/api'; async function testConnection() { const response = await api.asUser().requestGraph(` query { echo(myString: "Hello World!") } `); const data = await response.json(); console.log(data); // Should log: { data: { echo: "Hello World!" } } }
If this query succeeds, your app is properly configured and authenticated.
You can also test queries interactively using the GraphiQL Playground. Access it at:
1 2https://<your-site>.atlassian.net/gateway/api/graphql/twg
For example, if your site is abc.atlassian.net, the playground URL would be:
1 2https://abc.atlassian.net/gateway/api/graphql/twg
Required header: In the playground, add this header with your cloud ID:
1 2{ "X-Query-Context": "ari:cloud:jira::site/<your-cloud-id>" }
To find your cloud ID, you can use the Atlassian Cloud Admin API or extract it from your site's ARI.
The playground provides:
Now let's retrieve real data from the Graph. For this example, we'll fetch all teams that a user belongs to with the relationship User is in team.
1 2MATCH (user:IdentityUser {ari: $id})-[:user_is_in_team]->(team:IdentityTeam) RETURN collect(distinct team) as teams
Let's break this down:
MATCH - Finds patterns in the graph(user:IdentityUser {ari: $id}) - Finds a user object with a specific ARI (passed as a parameter)-[:user_is_in_team]-> - Traverses the "user is in team" relationship(team:IdentityTeam) - Finds connected team objectsRETURN collect(distinct team) as teams - Returns all unique teams as a listTo learn more about the user_is_in_team relationship, see the relationship documentation for User is in team.
An Atlassian Resource Identifier (ARI) is a globally unique identifier for objects in Teamwork Graph. In this query, we use the user's ARI to find their specific user object.
ARIs for users follow this pattern:
1 2ari:cloud:identity::user/{userId}
For example:
1 2ari:cloud:identity::user/712020:5fb4febcfacfd60076a1c699
Each object type's documentation provides instructions for how to construct and determine ARIs for that type. For more details about constructing a user's ARI, see the User object type documentation.
To execute the Cypher query and retrieve full object details, wrap it in a GraphQL query that uses the cypherQuery field.
1 2query UserTeams($cypherQuery: String!, $params: CypherRequestParams) { cypherQuery(query: $cypherQuery, params: $params) { edges { node { columns { key value { __typename ... on CypherQueryResultListNode { nodes { id data { __typename ... on Team { id displayName } } } } } } } } } }
The GraphQL query converts the raw Cypher results into fully-populated objects with all their fields.
Key points about the query structure:
CypherQueryResultListNode - Used because the Cypher query returns a list (via collect())CypherQueryResultNode - Would be used if the Cypher query returned a single objectdata { ... on Team } - Populates team objects with specific fields like id and displayNameYou can explore all available fields for teams in the Team object type documentation.
Pass the Cypher query and its parameters as GraphQL variables:
1 2{ "cypherQuery": "MATCH (user:IdentityUser {ari: $id})-[:user_is_in_team]->(team:IdentityTeam) RETURN collect(distinct team) as teams", "params": { "id": "ari:cloud:identity::user/712020:5fb4febcfacfd60076a1c699" } }
Now let's implement this query in a Forge app.
1 2// Makes an API call to TWG export async function executeCypherGraphQL(graphqlQuery, variables, headers) { console.log('executing cypher graphql', graphqlQuery, variables, headers); return __requestAtlassianAsUser('/graphql/twg',{ method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: graphqlQuery, variables: variables || {}, }) } ).then(async r => { console.log(`response status: ${r.status}`); const json = await r.json(); console.log(`response json: ${JSON.stringify(json)}`); return json; }); }
1 2// The GraphQL Query export const USER_TEAMS_GRAPHQL = ` query UserTeams($cypherQuery: String!, $params: CypherRequestParams) { cypherQuery(query: $cypherQuery, params: $params) { edges { node { columns { key value { __typename ... on CypherQueryResultListNode { __typename nodes { id data { __typename ... on Team { id displayName } } } } } } } } } } `; // Preparing the Variables export function getTeamsByUserRequestParams(userAri) { return { cypherQuery: ` MATCH (user:IdentityUser {ari: $id})-[:user_is_in_team]->(team:IdentityTeam) RETURN collect(distinct team) as teams`, params: {id: userAri}, }; }
1 2// Actual call given a user & context to fetch all the teams export async function fetchTeamsForAUser(userAri, context) { if (!userAri || typeof userAri !== 'string') { throw new Error('fetchTeamsForAUser: userAri is required'); } const { cypherQuery, params } = getTeamsByUserRequestParams(userAri); const json = await executeCypherGraphQL(USER_TEAMS_GRAPHQL_QUERY, { cypherQuery, params}, buildHeaders(context)); return extractCollections(json,['teams']); } // Helper method to builder Headers from Context function buildHeaders(context) { return { 'X-Query-Context': context }; } // Helper method to Extract Teams from the Json Output export function extractCollections(json, keys) { const result = keys.reduce((acc, k) => { acc[k] = []; return acc; }, {}); try { const edges = json?.data?.cypherQuery?.edges || []; for (const edge of edges) { const columns = edge?.node?.columns || []; for (const col of columns) { const key = col?.key; const value = col?.value; if (!key || !value || !(key in result)) continue; const pushItem = (data) => { if (!data) return; const item = {}; if (data.id) item.id = data.id; if (data.displayName) item.displayName = data.displayName; if (Object.keys(item).length) result[key].push(item); }; if (Array.isArray(value.nodes)) { for (const n of value.nodes) pushItem(n?.data); } else if (value.data) { pushItem(value.data); } } } } catch (e) { // fall through and return whatever we have } return result; }
You can place all of these functions in a single file (for example, teamwork-graph-client.js) and
import them wherever you need to query Teamwork Graph data in your app.
Use Forge tunnel to see real-time console logs from your app:
1 2forge tunnel
This lets you see the console.log and console.error output from your API calls in real time.
Error: Deployment failed: Missing required scope 'read:graph:jira'
Solution: Add the required scope to your manifest.yml.
For Jira apps:
1 2permissions: scopes: - 'read:graph:jira'
For Confluence apps:
1 2permissions: scopes: - 'read:graph:confluence'
Then redeploy:
1 2forge deploy
Error: Authentication failed or Unauthorized
Solution: Your app needs to be allowlisted for Teamwork Graph API access. Contact Atlassian
support with your app ID (found in your manifest.yml) to request access.
If you know data exists but your query returns empty results:
If the issue persists, it may be a data sync problem. Contact Atlassian support for assistance.
Common causes:
X-Query-Context headerDebug steps:
Now that you understand the basics of querying Teamwork Graph, you can explore more complex use cases and patterns.
The example apps below demonstrate real-world implementations that use advanced Cypher queries, multiple relationships, and integration with Forge UI components:

Forge Teamwork Graph Dashboards Widget
Atlassian Home Dashboards widget that track work contributions and project/goal rollups across multiple tools using Teamwork Graph.

Teamwork Graph-powered Onboarding Assistant
A Rovo agent that queries Teamwork Graph to gather team resources and onboard new starters.
Rate this page: