Add GraphQL endpoint for workflow-workflow links
What does this MR do and why?
Adds GraphQL support for Ai::DuoWorkflows::WorkflowWorkflow links (session to session). This follows the same pattern as the existing work item, merge request, note, and pipeline links.
-
New
DuoWorkflow.workflowLinksconnection, with an optionallinkTypefilter. -
New
DuoWorkflowWorkflowLinktype withlinkType,linkedWorkflow,workflow, andcreatedAtfields. -
New
DuoWorkflowWorkflowLinkTypeenum (currently onlySOURCE, meaning the session this one was restarted from). -
New
Ai::DuoWorkflows::WorkflowWorkflowPolicy, which uses the session's policy (same as the other link policies). -
linkedWorkflowusesDuoWorkflowTypeauthorization, so it returnsnullwhen the user can't read the linked session. -
New
DuoWorkflow.inverseWorkflowLinksconnection for the reverse direction (sessions that link to this one, for example the sessions restarted from it), backed by a newinverse_workflow_linksassociation onWorkflow. -
DuoWorkflownow declaresauthorize_granular_tokenforread_duo_workflowat the user boundary and is removed fromauthorization_todo.txt. Granular tokens were denied at the parent type, soworkflowLinkswas unreachable and the required granular-token spec passed with an empty node list.
Both directions use DuoWorkflowWorkflowLink. On an inverse link, workflow is the other session and linkedWorkflow is the queried session.
References
How to set up and validate locally
-
Run a developer flow locally that takes a while (giving you enough time to cancel it). e.g.
@duo-developer-gitlab-duo - Wait 2 min and then check the time and report back. -
On the right sidebar, open the session and cancel it.
-
Confirm the session is in cancelled state and note the workflow ID:
w = Ai::DuoWorkflows::Workflow.last
w.status_name
=> :stopped
w.id- From the command line, restart the session:
curl --request POST \
--header "PRIVATE-TOKEN: <GDK_PAT_TOKEN>" \
--url "http://gdk.test:3000/api/v4/ai/duo_workflows/workflows/<WORKFLOW_ID>/restart"-
When the session completes, note the new workflow ID as well.
-
Run this query in GraphQL with the second workflow ID, and confirm it has a link to the original workflow ID:
query {
duoWorkflowWorkflows(workflowId: "gid://gitlab/Ai::DuoWorkflows::Workflow/NEW_ID") {
nodes {
id
workflowLinks {
nodes {
linkType
createdAt
linkedWorkflow { id goal humanStatus }
}
}
}
}
}Example:
{
"data": {
"duoWorkflowWorkflows": {
"nodes": [
{
"id": "gid://gitlab/Ai::DuoWorkflows::Workflow/4904",
"workflowLinks": {
"nodes": [
{
"linkType": "SOURCE",
"createdAt": "2026-10-05T19:59:56Z",
"linkedWorkflow": {
"id": "gid://gitlab/Ai::DuoWorkflows::Workflow/4903",
...- Run this query with the original workflow ID, and confirm it has a link to the second (restarted) workflow ID:
query {
duoWorkflowWorkflows(workflowId: "gid://gitlab/Ai::DuoWorkflows::Workflow/ORIGINAL_ID") {
nodes {
id
inverseWorkflowLinks {
nodes {
linkType
createdAt
workflow { id }
linkedWorkflow { id goal humanStatus }
}
}
}
}
}Example:
{
"data": {
"duoWorkflowWorkflows": {
"nodes": [
{
"id": "gid://gitlab/Ai::DuoWorkflows::Workflow/4903",
"inverseWorkflowLinks": {
"nodes": [
{
"linkType": "SOURCE",
"createdAt": "2026-10-05T19:59:56Z",
"workflow": {
"id": "gid://gitlab/Ai::DuoWorkflows::Workflow/4904"
...Authorization note
DuoWorkflow gains authorize_granular_token permissions: :read_duo_workflow, boundary: :user, boundary_type: :user, matching WorkflowBranchesResolver and WorkflowLinkBaseType. The assignable permission group only allows the user boundary. DuoWorkflow was already gated by authorize :read_duo_workflow, so this grants nothing beyond what an api-scoped legacy PAT already had.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.