feat(duo_workflows): add status parameter on totalCount

What does this MR do and why?

Adds a status parameter on totalCount in the DuoWorkflows analytics source, so one query can carry the count of several flow states side by side as aliased metrics:

mode: analytics
query: type = DuoWorkflow and group = "my-group" and created > -30d
metrics: totalCount as "all", totalCount(status="paused") as "paused", totalCount(status="finished") as "completed", totalCount(status="stopped") as "aborted", totalCount(status="failed") as "failed"

compiles to

aggregated(orderBy: [{direction: DESC, identifier: "totalCount", parameters: {status: ["finished"]}}]) {
  nodes {
    totalCount
    totalCount_status_paused: totalCount(status: ["paused"])
    totalCount_status_finished: totalCount(status: ["finished"])
    totalCount_status_stopped: totalCount(status: ["stopped"])
    totalCount_status_failed: totalCount(status: ["failed"])
  }
}
  • The parameter is a list of the nine flow state names, one spelling authority with the status filter and dimension (FLOW_STATUSES). A single value is sugar for a one-item list; the list form (totalCount(status=["paused", "input_required"])) compiles as written.
  • A sort by an aliased instance carries its parameter into orderBy.parameters; verified live against gitlab-org on gitlab.com (see Example Usage).
  • Schema regenerated.

Two compiler rules change with it, for every analytics source:

  • A list parameter defaulting to [] is optional. Omitted, written as totalCount() or as totalCount(status=[]), the field resolves to the bare selection, so a plain totalCount compiles to a plain totalCount. Emitting status: [] would hand the engine an empty-array parameter, which ParameterizedDefinition#instance_key turns into a hashed instance key while Count#parameter_conditions treats it as no filter.
  • Duplicate base fields may include one instance without a generated response key. The bare field name never contains _ and every slug does, so totalCount as "all", totalCount(status="finished") as "completed" keeps distinct keys. A plain-name sort means that instance, whether or not it carries a label-only alias (sort: totalCount and sort: all are the same sort); with every instance parameterised and aliased the sort stays ambiguous and names the aliases. This also makes durationQuantile(0.5) as "p50", durationQuantile(0.8) legal for pipelines and MR analytics. Two unslugged instances are still rejected, with the message chosen by the instances themselves: identical parameters (totalCount as "a", totalCount as "b", merged, mergedAt) is "listed more than once", differing parameters (totalCount, totalCount(status="finished")) asks for a unique alias on the parameterised one.

Enum parameter values may contain the characters the slug sanitiser escapes (input_required slugs as totalCount_status_input_urequired).

Note for the DAP Impact Work tab: don't alias the bare count as "created"

created is also the name of the flow creation date dimension, so the alias from the issue's example (totalCount as "created") trips two existing checks:

  • sort: created is rejected as ambiguous: it could mean the creation date or the aliased count, and the compiler refuses to guess.
  • Beside a created(weekly) dimension the query is rejected outright (Alias "created" matches another selected field): the dimension's output column is already keyed created, and two columns cannot share a key.

Pick another alias for the bare count, e.g. totalCount as "all". Its data key stays totalCount whatever the alias, since an alias on a non-parameterised field is a label only.

The docs table for the source in gitlab-org/gitlab follows the release and bump.

Closes #215 (closed)

Example Usage

Build the Ruby extension once:

cd glql_rb
bundle install
bundle exec rake compile

Both examples read Duo flow analytics for gitlab-org, which needs a token with access to the group: export GITLAB_TOKEN=<your PAT> before running a snippet.

In the transformed rows the status columns land under their alias (fields[].key is completed, aborted, ...), while the bare count keeps totalCount as its key: an alias on a non-parameterised field is a label only.

Sessions by status, one column per state

mode: analytics
query: type = DuoWorkflow and group = "gitlab-org" and created > -30d
dimensions: flowType
metrics: totalCount as "all", totalCount(status="paused") as "paused", totalCount(status="finished") as "completed", totalCount(status="stopped") as "aborted", totalCount(status="failed") as "failed"
sort: completed desc
Test via ruby extension
cd glql_rb # after the build prerequisite above

bundle exec ruby -Ilib - <<'RUBY'
require "gitlab_query_language"
require "json"
require "net/http"

query   = 'type = DuoWorkflow and group = "gitlab-org" and created > -30d'
context = {
  mode: "analytics",
  dimensions: "flowType",
  metrics: 'totalCount as "all", totalCount(status="paused") as "paused", totalCount(status="finished") as "completed", totalCount(status="stopped") as "aborted", totalCount(status="failed") as "failed"',
  sort: "completed desc",
}

# 1. Compile GLQL -> GraphQL (also returns the typed `fields` the transform needs).
compiled = Glql.compile(query, context)
puts "== Generated GraphQL =="
puts compiled["output"]

# 2. Execute against GitLab. Analytics data is private, so the request is authenticated.
uri  = URI("https://gitlab.com/api/graphql")
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = true
req  = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req["PRIVATE-TOKEN"] = ENV.fetch("GITLAB_TOKEN")
req.body = { query: compiled["output"], variables: { limit: 3 } }.to_json
response = JSON.parse(http.request(req).body)

# 3. Transform the response back into flat rows. Pass response["data"] (the
#    GraphQL `data` object, not the whole envelope) and reuse the compiled
#    `fields`, which carry the resolved parameters/aliases.
transformed = Glql.transform(response["data"], { mode: context[:mode], fields: compiled["fields"] })
puts "\n== Transformed rows =="
puts JSON.pretty_generate(transformed["data"])
RUBY

Generated GraphQL:

query GLQL($before: String, $after: String, $limit: Int) {
  group(fullPath: "gitlab-org") {
    analytics {
      duoWorkflows(createdAtFrom: "2026-08-19 23:59") {
        aggregated(before: $before, after: $after, first: $limit, orderBy: [{direction: DESC, identifier: "totalCount", parameters: {status: ["finished"]}}]) {
          count
          pageInfo {
            startCursor
            endCursor
            hasNextPage
            hasPreviousPage
          }
          nodes {
            dimensions {
              flowType: workflowDefinition
            }
            totalCount
            totalCount_status_paused: totalCount(status: ["paused"])
            totalCount_status_finished: totalCount(status: ["finished"])
            totalCount_status_stopped: totalCount(status: ["stopped"])
            totalCount_status_failed: totalCount(status: ["failed"])
          }
        }
      }
    }
  }
}

Transformed rows (truncated to the aliased keys):

{
  "count": 30,
  "nodes": [
    { "flowType": "ai_catalog_agent", "totalCount": 16764, "paused": 0, "completed": 16274, "aborted": 14, "failed": 467 },
    { "flowType": "code_review/v1",   "totalCount": 15873, "paused": 0, "completed": 13506, "aborted": 59, "failed": 2288 },
    { "flowType": "developer/v1",     "totalCount": 64027, "paused": 0, "completed": 2468,  "aborted": 337, "failed": 29801 }
  ]
}

Flows waiting on the user per week, list form

mode: analytics
query: type = DuoWorkflow and group = "gitlab-org" and created > -30d
dimensions: created(weekly)
metrics: totalCount, totalCount(status=["paused", "input_required"]) as "waiting"
sort: created desc
Test via ruby extension
cd glql_rb # after the build prerequisite above

bundle exec ruby -Ilib - <<'RUBY'
require "gitlab_query_language"
require "json"
require "net/http"

query   = 'type = DuoWorkflow and group = "gitlab-org" and created > -30d'
context = {
  mode: "analytics",
  dimensions: "created(weekly)",
  metrics: 'totalCount, totalCount(status=["paused", "input_required"]) as "waiting"',
  sort: "created desc",
}

# 1. Compile GLQL -> GraphQL (also returns the typed `fields` the transform needs).
compiled = Glql.compile(query, context)
puts "== Generated GraphQL =="
puts compiled["output"]

# 2. Execute against GitLab. Analytics data is private, so the request is authenticated.
uri  = URI("https://gitlab.com/api/graphql")
http = Net::HTTP.new(uri.host, uri.port); http.use_ssl = true
req  = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
req["PRIVATE-TOKEN"] = ENV.fetch("GITLAB_TOKEN")
req.body = { query: compiled["output"], variables: { limit: 3 } }.to_json
response = JSON.parse(http.request(req).body)

# 3. Transform the response back into flat rows. Pass response["data"] (the
#    GraphQL `data` object, not the whole envelope) and reuse the compiled
#    `fields`, which carry the resolved parameters/aliases.
transformed = Glql.transform(response["data"], { mode: context[:mode], fields: compiled["fields"] })
puts "\n== Transformed rows =="
puts JSON.pretty_generate(transformed["data"])
RUBY

Generated GraphQL:

query GLQL($before: String, $after: String, $limit: Int) {
  group(fullPath: "gitlab-org") {
    analytics {
      duoWorkflows(createdAtFrom: "2026-08-19 23:59") {
        aggregated(before: $before, after: $after, first: $limit, orderBy: [{direction: DESC, identifier: "createdAt", parameters: {granularity: "weekly"}}]) {
          count
          pageInfo {
            startCursor
            endCursor
            hasNextPage
            hasPreviousPage
          }
          nodes {
            dimensions {
              createdAt(granularity: "weekly")
            }
            totalCount
            totalCount_status_paused_0input_urequired: totalCount(status: ["paused", "input_required"])
          }
        }
      }
    }
  }
}

Transformed rows (truncated to the aliased keys):

{
  "count": 5,
  "nodes": [
    { "created": "2026-09-14", "totalCount": 23568, "waiting": 8481 },
    { "created": "2026-09-07", "totalCount": 28799, "waiting": 10991 },
    { "created": "2026-08-31", "totalCount": 25288, "waiting": 6879 }
  ]
}

How to set up and validate locally

  1. cargo test --test duo_workflows_tests total_count_status
  2. Run either snippet under Example Usage with a GITLAB_TOKEN that can read gitlab-org analytics; each alias returns its own count and the plain totalCount selection is unchanged.
  3. cargo run --bin generate-schema && npm run lint:prettier:fix produces no diff.
Edited by Daniele Rossetti

Merge request reports

Loading
Loading