Script Jobs allow you to run your own code at regular intervals. Some examples are:

There is a built-in scheduled job type that allows you to iterate over content defined by a CQL query, and do something with each hit.

It is not advisable to use this on Data Center at the moment, as currently it will run on multiple cluster nodes.

CQL Escalation Service

There is a specialised case of a scheduled job, where the results of a CQL query are passed to the code you provide. It saves you having to write the code to execute the query, and set the impersonation context.

In the script binding a variable hits is available, which is an Iterable<ContentEntityObject>. Therefore you can do something with each piece of content returned using:

hits.each { content ->
    // do something with content

Depending on your query, the type of content in the above example will be either a Page, an Attachment, a BlogPost, or a Comment etc.

However, it will always be a ContentEntityObject.

If you only want to do something with pages, then use a query that only selects pages, eg space = ds and type = page.

Alternatively, you can use instanceof in your script to do different things depending on the type of content.

You should specify a user that has permission to see the content that you want.
Your service code is automatically wrapped into a transaction template, but you should take care your service doesn’t not run for very long, unless it’s at a quiet time, e.g. the weekend.


Add label to outdated pages

As a worked example, we’ll add a label pages that haven’t been modified for one month in a particular space. You may do this if you need to ensure that content is kept current, although this solution is deliberately over-simplistic.

add label job config

The query here selects pages (and only pages, not attachments, blog posts, comments etc) in the demonstration space that have not been modified for over 4 weeks. Look at the examples and refer to the CQL documentatation if you need help.

Note that the number of hits from the query is shown. You should be careful that you don’t inadvertently more content than you intended to. The user to run the query as is also shown highlighted.

Clicking Run now will run the service right now, so you can test (on a development instance ideally).

The content of the file in the screenshot is below - you can either copy it as an inline script, or put it on a file under a script root.

import com.atlassian.confluence.labels.Label
import com.atlassian.confluence.labels.LabelManager
import com.atlassian.confluence.labels.Namespace
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.scheduler.JobRunnerResponse

def labelManager = ComponentLocator.getComponent(LabelManager)
hits.each { page -> (1)

    def labelName = "requires-review"

    def label = labelManager.getLabel(labelName)
    if (!label) {
        label = labelManager.createLabel(new Label(labelName, Namespace.GLOBAL));

    if (!page.labels*.name.contains(labelName)) {"Add label to page: ${page.title}")
        labelManager.addLabel(page, label) (2)

return JobRunnerResponse.success()
1 iterate over the items returned from the CQL
2 add label to the page
Add comment to outdated pages

Alternatively you may wish to add a comment to any page that has not been modified for over three months, and has had no comment in over three months. This is very similar to the example above so refer to that for any missing information.

The query would be:

space = ds and type = page and lastModified < now("-14w")
CQL does not support searching on months or years, so multiply the weeks value appropriately.

The code is:

import com.atlassian.confluence.pages.CommentManager
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.scheduler.JobRunnerResponse

def commentManager = ComponentLocator.getComponent(CommentManager)

hits.each { page ->

    if (! commentManager.getPageComments(, new Date() - 98)) { (1)
        log.debug("Adding 'old page' comment to page: ${page.title}")
        commentManager.addCommentToObject(page, null, "Old page! Please review")

return JobRunnerResponse.success()
1 check we don’t have a comment added within the last three months (in days)

Prune Page Versions

Over time, the number of revisions for each page will grow. Do you really need versions that were created over two years ago? Given that Confluence stores the entire content for each revision (rather than deltas), this can dramatically increase your database storage requirements.

This scheduled job will allow you to automatically prune old versions according to your requirements, for example:

  • On all spaces, remove versions older than three months, but always keep the latest three versions

  • On spaces where you may have sensitive content, only keep the latest revision

This will irretrievably remove versions according to your rules. They cannot be restored without doing a full space or Confluence restore. Make sure you have tested on your staging instance. At the very least start with a CQL query that selects just test content.


Start by writing CQL to select the content you are interested in. See the provided examples to select only content from particular spaces etc.

Next specify your parameters for which versions to remove. You must enter at least one of the following, but you can provide both:

  • Minimum number of versions to retain. If no Older than days is provided, it will just delete all versions whilst retaining the latest X

  • If Older than days is provided on its own, all versions older than this number will be removed. If you also provide a minimum number of versions to retain, it will delete the older versions, but only those over the minimum number to retain

For example, to remove versions older than 90 days, but ensure there are always two historical versions, you may use this configuration:

prune content hr
It’s possible to have a general configuration for all spaces, for example, keep all versions made within the last three months and a minimum of three, but more restrictive settings for certain spaces. You would do this simply by setting up two scheduled jobs.
This current only works for blog posts and pages, support for atttachments will be delivered soon.
The most recent version will never be deleted.