How to bulk delete GitLab artifacts using GraphQL API

Issue #1005

GitLab CI/CD pipelines create artifacts that take up a lot of storage space. While GitLab has a UI to delete them, doing this for thousands of artifacts one page at a time is slow and boring.

In this article, we’ll build a script that uses GitLab’s GraphQL API to delete artifacts automatically.

Image

Why Use GraphQL API?

The GraphQL API allow us to:

  • Get data with pagination
  • Delete up to 20 artifacts in one request
  • Get exactly what you need

When you delete artifacts in GitLab’s web page, it uses the same GraphQL API we’ll use. Open your browser’s Network tab and click “Delete” - you’ll see it calling bulkDestroyJobArtifacts.

What You Need

  • Access Token: Create one in GitLab Settings → Access Tokens with api scope
  • Project ID: Found in Project Settings → General (e.g., 558)
  • Project Path: Your repo path (e.g., your-org/your-repo)
  • GitLab URL: Your GitLab URL (e.g., https://gitlab.com)

Let’s build this step by step.

Step 1: Setup

First, set up your config:

async function deleteAllArtifacts() {
  // Update these values
  const projectId = `gid://gitlab/Project/558`;
  const projectPath = `your-org/your-repo`;
  const accessToken = `glpat-YOUR_TOKEN_HERE`;
  const baseUrl = `https://gitlab.com`;

  // Track progress
  let totalDeleted = 0;
  let hasMore = true;
  let afterCursor = null;
  let pageNum = 1;

  // Code continues below...
}

What each line does:

  • projectId: Tells GitLab which project to use
  • projectPath: Alternative way to identify the project
  • accessToken: Your access token for authentication
  • totalDeleted: Counts how many artifacts we’ve deleted
  • afterCursor: Remembers where we are in the list (for pagination)

Step 2: Main Loop

The script runs in a loop until all artifacts are deleted:

while (hasMore) {
  console.log(`\n=== Processing batch ${pageNum} ===`);

  // 1. Fetch jobs
  // 2. Get artifact IDs
  // 3. Delete artifacts
  // 4. Check if more pages exist
}

Step 3: Fetch Jobs

Get 100 jobs at a time using GraphQL:

const queryVariables = {
  fullPath: projectPath,
  first: 100
};

if (afterCursor) {
  queryVariables.after = afterCursor;
}

const queryResponse = await fetch(`${baseUrl}/api/graphql`, {
  method: `POST`,
  headers: {
    "Content-Type": `application/json`,
    "Authorization": `Bearer ${accessToken}`
  },
  body: JSON.stringify({
    query: `query($fullPath: ID!, $first: Int!, $after: String) {
      project(fullPath: $fullPath) {
        jobs(first: $first, after: $after) {
          pageInfo {
            hasNextPage
            endCursor
          }
          nodes {
            id
            artifacts {
              nodes {
                id
              }
            }
          }
        }
      }
    }`,
    variables: queryVariables
  })
});

How this works:

  • Get 100 jobs per request
  • Use afterCursor to start from where we left off
  • Get artifact IDs from each job
  • Get pagination info to know if more jobs exist

About Pagination:

We use cursor-based pagination. Think of it like bookmarks:

  • First request: “Give me first 100 jobs” → Get cursor “abc123”
  • Second request: “Give me 100 jobs after abc123” → Get cursor “def456”
  • Last request: “Give me 100 jobs after xyz789” → No more jobs

This is better than counting (page 1, 2, 3) because if items are deleted while we’re running, we won’t miss anything or get duplicates.

Step 4: Check Response

Always check if the request worked:

const queryResult = await queryResponse.json();

if (queryResult.errors) {
  console.error(`Errors:`, queryResult.errors);
  break;
}

if (!queryResult.data || !queryResult.data.project) {
  console.error(`No data returned`);
  break;
}

const jobsData = queryResult.data.project.jobs;
const jobs = jobsData.nodes;
const pageInfo = jobsData.pageInfo;

console.log(`Found ${jobs.length} jobs`);

Step 5: Get Artifact IDs

Loop through jobs and collect artifact IDs:

const artifactIds = [];

for (const job of jobs) {
  if (job.artifacts && job.artifacts.nodes) {
    for (const artifact of job.artifacts.nodes) {
      artifactIds.push(artifact.id);
    }
  }
}

console.log(`Found ${artifactIds.length} artifacts`);

Each job can have many artifacts. We collect all IDs into one array.

Step 6: Delete in Batches

GitLab lets you delete 20 artifacts at a time. We split our list into groups of 20:

if (artifactIds.length > 0) {
  for (let i = 0; i < artifactIds.length; i += 20) {
    const batch = artifactIds.slice(i, i + 20);

    // Delete this batch (next step)
  }
}

Why 20 at a time?

  • GitLab’s limit: max 20 per request
  • Better progress tracking: see results after each batch
  • Safer: if one batch fails, others still work

Step 7: Delete Mutation

Send the delete request:

const deleteResponse = await fetch(`${baseUrl}/api/graphql`, {
  method: `POST`,
  headers: {
    "Content-Type": `application/json`,
    "Authorization": `Bearer ${accessToken}`
  },
  body: JSON.stringify({
    operationName: `bulkDestroyJobArtifacts`,
    variables: {
      projectId: projectId,
      ids: batch
    },
    query: `mutation bulkDestroyJobArtifacts($projectId: ProjectID!, $ids: [CiJobArtifactID!]!) {
      bulkDestroyJobArtifacts(input: {projectId: $projectId, ids: $ids}) {
        destroyedCount
        destroyedIds
        errors
      }
    }`
  })
});

This tells GitLab: “Delete these artifacts from this project”.

Step 8: Check Delete Results

See how many were deleted:

const result = await deleteResponse.json();

if (result.data && result.data.bulkDestroyJobArtifacts) {
  const count = result.data.bulkDestroyJobArtifacts.destroyedCount || 0;
  totalDeleted += count;
  console.log(`  ✅ Deleted ${count} artifacts (Total: ${totalDeleted})`);

  if (result.data.bulkDestroyJobArtifacts.errors?.length > 0) {
    console.error(`  ❌ Errors:`, result.data.bulkDestroyJobArtifacts.errors);
  }
}

// Wait 500ms before next batch (be nice to the server)
await new Promise(resolve => setTimeout(resolve, 500));

Why wait 500ms?

  • Prevents overloading GitLab’s servers
  • Avoids getting blocked for too many requests
  • GitLab can process each request properly

Step 9: Move to Next Page

After deleting all artifacts in this batch of jobs, check if there are more:

if (pageInfo.hasNextPage) {
  afterCursor = pageInfo.endCursor;
  pageNum++;
  console.log(`Moving to next page`);
  await new Promise(resolve => setTimeout(resolve, 1000));
} else {
  hasMore = false;
  console.log(`No more jobs`);
}

Wait 1 second between job pages to be respectful of the API.

Image
=== Batch 1 ===
Found 100 jobs
Found 45 artifacts
  ✅ Deleted 20 (Total: 20)
  ✅ Deleted 20 (Total: 40)
  ✅ Deleted 5 (Total: 45)

=== Batch 2 ===
Found 100 jobs
Found 32 artifacts
  ✅ Deleted 20 (Total: 65)
  ✅ Deleted 12 (Total: 77)

🎉 Done! Deleted 77 artifacts

Conclusion

This script automates GitLab artifact deletion using the official GraphQL API. It handles large numbers of artifacts automatically, shows progress, and respects API limits.

You can adapt this for other GitLab tasks like managing pipelines, cleaning branches, or analyzing CI/CD data.

Further Reading

Written by

I’m open source contributor, writer, speaker and product maker.

Start the conversation