Developer
News and Updates
Get Support
Sign in
Get Support
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Sign in
DOCUMENTATION
Cloud
Data Center
Resources
Sign in
Last updated Sep 15, 2026

Migrate from raw Cypher queries

Apps can no longer send arbitrary Cypher text to Teamwork Graph at request time. If your app calls the cypherQuery field with a query string it builds at runtime, you need to migrate.

There are two destinations, and you should try them in this order:

  1. Adopt a curated query. Teamwork Graph ships supported, typed GraphQL fields for the most common traversals. If one covers your use case this is the least work and the most stable option — no registration step and no propagation delay.
  2. Persist your Cypher query. If no curated query fits, register your Cypher text once and execute it by key. Your traversal keeps working, but the query is validated and stored ahead of time instead of being sent on every request.

This page helps you work out which path each of your call sites takes. The two pages linked above are the reference documentation for actually doing it.

Migration timeline

Raw cypherQuery is unavailable to new apps. Apps that used it during the Early Access Program keep access for a limited migration window. When that window closes, cypherQuery stops accepting traffic and any call site still using it starts failing.

The cut-off date is announced in the Teamwork Graph changelog. Nothing on either migration path depends on that date, so you can move your call sites now.

Why this is changing

Accepting arbitrary Cypher at request time means every call is an unbounded, unreviewed graph traversal. That makes it impossible to guarantee latency, to plan capacity, or to protect the graph from a query that accidentally fans out across a whole site. It also means the underlying graph schema can't evolve safely, because any change might break a query Atlassian has never seen.

Knowing the queries ahead of time fixes both problems. A query can be validated once and its plan cached, and you find out before you ship if it won't work.

Step 1: Inventory your raw Cypher calls

Look for requests to the Teamwork Graph GraphQL endpoint that select the cypherQuery field:

1
2
POST https://yourinstancename.atlassian.net/gateway/api/graphql/twg

The field you're migrating off looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
query companyName_assignedWork($cypherQuery: String!, $params: CypherRequestParams) {
  cypherQuery(query: $cypherQuery, params: $params, first: 10) {
    edges {
      node {
        columns {
          key
          value {
            __typename
            ... on CypherQueryResultNode {
              id
            }
          }
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

For each call site, write down three things:

  • The Cypher text, including the starting object type, the relationship it follows, and what it returns.
  • The parameters you pass.
  • The response shape your code reads.

You need all three whichever path you take, and the first one is what decides the path.

Step 2: Choose a path per call site

Compare each traversal against the curated query catalog. A curated query is a match if it starts from the same kind of object, follows the same relationship, and returns the same kind of object.

Your situationPath
A curated query matches your traversalCurated query
A curated query almost matches, but you need extra filteringCurated query, filtering client-side
You only ever fetch one known object by ARIDirect object querying
Your traversal isn't in the catalogPersisted Cypher
You build Cypher text dynamically per requestPersisted Cypher, one query per distinct shape

Two things catch people out at this step.

Filtering client-side is usually the right call when a curated query is close but not exact. Curated queries return at most 25 rows, so the volume you would filter is small, and you keep the stability guarantee that comes with a supported field.

Dynamically built Cypher needs one persisted query per shape. Persisted queries can be parameterized by value but not by label or relationship type. If your app concatenates query text, enumerate the distinct traversals it can produce, register each one, and choose between the keys in your own code.

Step 3: Rewrite the call

To a curated query

The example from step 1 becomes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
query companyName_assignedWork($userId: ID!) {
  atlassianUserAssignedJiraWorkItems(userId: $userId, first: 25) {
    edges {
      node {
        ... on JiraWorkItem {
          id
          title
          status {
            name
          }
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

The response shape changes in two ways. The object arrives directly on the edge's node as a union member, so you select it with an inline fragment on the concrete type instead of walking columns[].key and columns[].value. And because it's typed, you request its fields directly rather than hydrating an ARI yourself.

Watch the page size. Curated queries cap first at 25, where raw Cypher allowed up to 1000, so any call site that pulled large pages needs a pagination loop.

See Curated queries for the catalog, the response format, and pagination.

To a persisted Cypher query

Register the query text you wrote down in step 1, once, at build or install time:

1
2
3
POST https://yourinstancename.atlassian.net/gateway/api/twg/cypher-queries
Content-Type: application/json
1
2
3
4
5
6
7
{
  "appId": "ari:cloud:ecosystem::app/your-app-id",
  "name": "assigned-work-items",
  "cypher": "MATCH (user:AtlassianUser {ari: $userId})-[:atlassian_user_assigned_jira_work_item]->(item:JiraWorkItem) RETURN item",
  "paramsSchema": [{ "name": "userId", "type": "ARI", "required": true }]
}

Keep the queryKey it returns, then call the query by key at runtime on the same GraphQL endpoint you use today, swapping the cypherQuery field for customQuery:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
query companyName_assignedWork($queryKey: String!, $queryParams: CypherRequestParams) {
  customQuery(queryKey: $queryKey, queryParams: $queryParams, first: 25) {
    edges {
      node {
        columns {
          key
          value {
            __typename
            ... on CypherQueryResultNode {
              id
            }
          }
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

This is the lower-effort rewrite if you're parsing raw Cypher responses today. customQuery returns the same CypherQueryConnection type as cypherQuery, so the only changes are the field name, the query key replacing the Cypher text, and queryParams replacing params.

See Persisted Cypher queries for the full request and response reference, the propagation window, and error handling.

Comparison

Raw Cypher (removed)Curated queryPersisted Cypher
Query text sent per requestYesNoNo
Setup step neededNoneNoneOne-time submission
Available immediatelyYesYesAfter about 1 minute of propagation
Custom traversalsYesNoYes
Max page size10002525
Typed, hydrated resultsManualYesManual, same as raw Cypher
Validated before runtimeNoYes, by constructionYes, at submission
EndpointcypherQuery fieldIts own root fieldcustomQuery field
Response shapeRows and columnsTyped union on nodeRows and columns, unchanged

Migration checklist

  • Every cypherQuery call site is inventoried, with its Cypher text, parameters, and response handling.
  • Each call site is assigned a path against the curated catalog.
  • Persisted queries are registered at build or install time, not during a user request.
  • Query keys are stored in app configuration, not hardcoded per environment by hand.
  • Pagination loops are driven by pageInfo.hasNextPage, and any call site that requested more than 25 rows from a curated query has been reworked.
  • Response parsing is updated for the shape of the path you chose.
  • No remaining code path constructs Cypher text at request time.

Frequently asked questions

What if no curated query fits and my traversal can't be fixed at registration time?

It almost certainly can. Parameters are supplied at execution time, so a query only needs registering once per traversal shape, not once per set of values. If you genuinely need a different shape per request, get in touch — that's worth a conversation before you build around it.

Can I still use cypherQueryV2 on the main GraphQL gateway?

That's a separate Graph Store field. It's marked experimental and requires an explicit @optIn, so it carries no stability guarantee and isn't a safe migration target for app code. Use a curated query or a persisted query.

Will my results change after migrating?

Permission filtering is unchanged — all three paths filter every row for the authenticated user. What can change is volume per page, because curated queries cap pages at 25.

Getting help

If you're unsure whether a curated query covers your use case, or a query you expect to be valid is rejected at submission, ask in the Atlassian developer community or raise a ticket in the Atlassian Ecosystem Help Center.

Rate this page: