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:
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.
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.
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.
Look for requests to the Teamwork Graph GraphQL endpoint that select the cypherQuery field:
1 2POST 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 22query 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:
You need all three whichever path you take, and the first one is what decides the path.
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 situation | Path |
|---|---|
| A curated query matches your traversal | Curated query |
| A curated query almost matches, but you need extra filtering | Curated query, filtering client-side |
| You only ever fetch one known object by ARI | Direct object querying |
| Your traversal isn't in the catalog | Persisted Cypher |
| You build Cypher text dynamically per request | Persisted 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.
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 20query 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.
Register the query text you wrote down in step 1, once, at build or install time:
1 2 3POST 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 22query 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.
| Raw Cypher (removed) | Curated query | Persisted Cypher | |
|---|---|---|---|
| Query text sent per request | Yes | No | No |
| Setup step needed | None | None | One-time submission |
| Available immediately | Yes | Yes | After about 1 minute of propagation |
| Custom traversals | Yes | No | Yes |
| Max page size | 1000 | 25 | 25 |
| Typed, hydrated results | Manual | Yes | Manual, same as raw Cypher |
| Validated before runtime | No | Yes, by construction | Yes, at submission |
| Endpoint | cypherQuery field | Its own root field | customQuery field |
| Response shape | Rows and columns | Typed union on node | Rows and columns, unchanged |
cypherQuery call site is inventoried, with its Cypher text, parameters, and response
handling.pageInfo.hasNextPage, and any call site that requested more than
25 rows from a curated query has been reworked.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.
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: