ScriptRunner allows you to respond to Confluence events, via an inline script or pointer to a file.

You could respond to events, for example in order to:

Adding an Event Handler

Navigate to Admin → Script Event Handlers. Click a heading to add a handler. Choose Custom Event Handler to use your own scripts to respond to events.

In the Events text field, start typing to find the events you want to listen for.

Some of the built-in content will have different options, and may not ask for the events to listen for.

add event handler

Working with Custom Event Handlers

The event is contained in the script binding. The event object will have the type corresponding to its name…​ for instance if you are listening for PageCreateEvent you will get a PageCreateEvent object.

You may choose to have your handler listen for multiple different events. If you need to do different things depending on the type of event, you can check that with instanceof.

Alternatively, you can type your event to the most specific superclass…​ in the above example that would be PageEvent.

For example, to get the page content for both PageCreateEvent and PageUpdateEvent:

def event = event as PageEvent(1)
def content = event.content.bodyAsString

// do something with the page content
1 - event is passed in the binding - this line is only used to give type information when using an IDE, and has no functional impact

Samples

Add a comment on page create

Some organisations have a particular style guide (for example The Guardian/Observer style guide, or would like to always write product names with the correct capitalisation.

This contrived, and not very useful example, looks at the content of new pages. If the page content contains any of a list of banned words a comment is automatically added with an alternative suggestion:

draft story

The code is:

import com.atlassian.confluence.event.events.content.page.PageEvent
import com.atlassian.confluence.pages.CommentManager
import com.atlassian.sal.api.component.ComponentLocator

def event = event as PageEvent
// use event.getPage().getSpace() if you want to restrict only to certain spaces

def commentManager = ComponentLocator.getComponent(CommentManager)
def body = event.content.bodyAsString

def alternatives = [
    "air hostess"     : "flight attendant",
    "amuck"           : "amok",
    "assisted suicide": "assisted dying",
]

def commentBody = alternatives.findAll { badWord, goodWord ->
    body.contains(badWord)
}.collect { badWord, goodWord ->
    "<li><b>${badWord}</b> should be avoided. Consider using: <b>${goodWord}</b>.</li>"
}.join("")

if (commentBody) {
    commentManager.addCommentToObject(event.content, null, "<p>Please consider the following issues: <ul>$commentBody</ul> </p>")
}
In practice you would also want to watch page updates, and then only look at the diff between old and new versions.

Add inline comment on page create

Slightly more useful is to add inline comments, as the author gets feedback about the words they should have been using, and are able to dismiss them.

draft story 2

The code is:

import com.atlassian.confluence.event.events.content.page.PageEvent
import com.atlassian.confluence.setup.settings.SettingsManager
import com.atlassian.sal.api.component.ComponentLocator
import groovy.json.JsonBuilder
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.Method
import net.sf.hibernate.HibernateException
import net.sf.hibernate.Session
import net.sf.hibernate.SessionFactory
import org.apache.http.HttpRequest
import org.apache.http.HttpRequestInterceptor
import org.apache.http.protocol.HttpContext
import org.apache.log4j.Level
import org.apache.log4j.Logger
import org.springframework.orm.hibernate.SessionFactoryUtils

import java.sql.SQLException

import static groovyx.net.http.ContentType.JSON

log = Logger.getLogger("com.test.InlineScript")
log.setLevel(Level.DEBUG)

event = event as PageEvent

def settingsManager = ComponentLocator.getComponent(SettingsManager)

baseUrl = settingsManager.getGlobalSettings().getBaseUrl()

def alternatives = [
    "air hostess"     : "flight attendant",
    "amuck"           : "amok",
    "assisted suicide": "assisted dying"
]

Session s = SessionFactoryUtils.getSession(ComponentLocator.getComponent(SessionFactory), false)
flushAndCommitSession(s)

def flushAndCommitSession(Session s) {
    // commit any open transaction to release any locks, as the tables get deleted
    // via another connection
    if (s != null) {
        try {
            log.info("Flushing session and committing pending transactions")
            s.flush()
            s.connection().commit()
            log.info("Session flush and commit complete")
        } catch (HibernateException he) {
            log.error("error flushing session", he)
        } catch (SQLException sqle) {
            log.error("error committing connection", sqle)
        }
    }
}

/**
 * find occurrence of word in a String
 */
Integer keyFinder(String pageBody, String key) {
    def keyToFind = /$key/
    def keyFinder = (pageBody =~ /$keyToFind/)
    keyFinder.count
}

/**
 * Recursive function for creating inline comments
 */
def createInlineComments(Map<String, String> altMap, String pageContent, Integer matches = 0, Integer index = 0, String key = "") {

    if (matches != 0 && matches == index && key) {
        //loop breakout condition or reset variables when key changes
        altMap.remove(key)
        if (altMap.size() == 0) {
            return
        }

        def keys = altMap.keySet() as List
        key = keys.pop()
        matches = keyFinder(pageContent, key)
        index = 0
    }
    else {
        //function entry point match count
        matches = keyFinder(pageContent, key)
    }
    if (altMap.size() == 0) {
        return
    }

    if (matches) {
        log.debug("Found key : ${key}")
        def data = [
            containerId         : "${event.page.getId()}",
            parentCommentId     : 0,
            numMatches          : matches,
            matchIndex          : index,
            body                : "<p>Please use <b>${altMap.get(key)}</b> instead</p>",
            originalSelection   : key,
            serializedHighlights: ""
        ]

        def jsonFormattedData = new JsonBuilder(data).toString()
        def http = new HTTPBuilder(baseUrl)

        http.client.addRequestInterceptor(new HttpRequestInterceptor() {
            void process(HttpRequest httpRequest, HttpContext httpContext) {
                httpRequest.addHeader('Authorization', 'Basic ' + 'admin:admin'.bytes.encodeBase64().toString())
                httpRequest.addHeader('X-Atlassian-Token', "no-check")
            }
        })

        http.request(Method.POST, JSON) {
            uri.path = "/confluence/rest/inlinecomments/1.0/comments"
            requestContentType = JSON
            body = jsonFormattedData

            response.success = { resp ->
                log.debug("RESULT STATUS:  DONE")
                //recursive call to the function
                createInlineComments(altMap, pageContent, matches, index + 1, key)
            }

            response.failure = { resp ->
                log.debug("FAILED : ${resp.statusLine.statusCode}")
            }
        }
    }
    else {
        altMap.remove(key)
        if (altMap.size() == 0) {
            return
        }
        createInlineComments(altMap, pageContent, 0, 0, (altMap.keySet() as List).pop())
    }
}

createInlineComments(alternatives, event.content.bodyAsString, 0, 0, (alternatives.keySet() as List).pop())

Create page when user created

This example automatically creates a "user profile" page in the TEAM space, which the user should then fill out with their skills and profile.

create user

The event selected is UserCreateEvent, the code is:

import com.atlassian.confluence.core.DefaultSaveContext
import com.atlassian.confluence.event.events.user.UserCreateEvent
import com.atlassian.confluence.pages.Page
import com.atlassian.confluence.pages.PageManager
import com.atlassian.confluence.spaces.SpaceManager
import com.atlassian.confluence.user.ConfluenceUser
import com.atlassian.sal.api.component.ComponentLocator
import groovy.xml.MarkupBuilder

try {
    def event = event as UserCreateEvent
    def user = event.user as ConfluenceUser
    def pageManager = ComponentLocator.getComponent(PageManager)

    def spaceManager = ComponentLocator.getComponent(SpaceManager)
    def teamSpace = spaceManager.getSpace("TEAM")

    def writer = new StringWriter()
    def builder = new MarkupBuilder(writer)
    builder.table {
        tbody {
            tr {
                td("About")
                td {
                    "ac:link" {
                        "ri:user"("ri:userkey": user.key)
                    }
                }
            }
            tr {
                td("Profile")
                td("")
            }
            tr {
                td("Skillz")
                td("")
            }
        }
    }

    def parentPage = teamSpace.getHomePage()
    assert parentPage

    def targetPage = new Page(title: "About ${user.fullName}",
        bodyAsString: writer.toString(),
        space: teamSpace,
        parentPage: parentPage
    )
    pageManager.saveContentEntity(targetPage, DefaultSaveContext.DEFAULT)
    parentPage.addChild(targetPage)
    pageManager.saveContentEntity(parentPage, DefaultSaveContext.MINOR_EDIT)
}
catch (anyException) {
    log.warn("Failed to create page for new user", anyException)
}

Collecting stats

This example automatically sends stats to statsd for page views, space views and users/pages views.

page views statsd

In the example we use Grafana to visualise our page views metrics.

The event selected is PageViewEvent, the code is:

import com.atlassian.confluence.event.events.content.page.PageEvent
import com.atlassian.confluence.user.AuthenticatedUserThreadLocal
import groovy.transform.Field

@Field final def host = "http://192.168.59.103/"
@Field final def port = 8125

def event = event as PageEvent
def currentUser = AuthenticatedUserThreadLocal.get()

// keys to create unique nodes for counters
def spaceKey = event.page.spaceKey
def pageId = event.page.id as String
def userKey = currentUser.name
def nodeId = "confluence.stats.views"

// build the unique metric keys
def pageViewMetricKey = "${nodeId}.page.${pageId}"
def spaceViewMetricKey = "${nodeId}.space.${spaceKey}"
def userViewMetricKey = "${nodeId}.user.${userKey}.${pageId}"

// increase by one the counters for the following metric keys
increaseByOne(pageViewMetricKey, userViewMetricKey, spaceViewMetricKey)

def increaseByOne(String... keys) {
    def dataToSend = ""
    def value = 1 //increase counter by one

    //syntax for counter according to https://github.com/etsy/statsd/blob/master/docs/metric_types.md
    for (key in keys) {
        dataToSend += "${key}:${value}|c\n"
    }

    def data = dataToSend.getBytes()
    def address = InetAddress.getByName(host as String)
    def packet = new DatagramPacket(data, data.length, address, port as int)
    def socket = new DatagramSocket()
    try {
        socket.send(packet)
    } finally {
        socket.close()
    }
}

Creating a Jira project whenever a Confluence space is created

This example automatically creates a Jira project every time a space is created

The event selected is SpaceCreateEvent, the code is:

import com.atlassian.applinks.api.ApplicationLinkService
import com.atlassian.applinks.api.application.jira.JiraApplicationType
import com.atlassian.confluence.event.events.space.SpaceCreateEvent
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.sal.api.net.Response
import com.atlassian.sal.api.net.ResponseException
import com.atlassian.sal.api.net.ResponseHandler
import groovy.json.JsonBuilder

import static com.atlassian.sal.api.net.Request.MethodType.POST

def appLinkService = ComponentLocator.getComponent(ApplicationLinkService)
def appLink = appLinkService.getPrimaryApplicationLink(JiraApplicationType)
def applicationLinkRequestFactory = appLink.createAuthenticatedRequestFactory()

def event = event as SpaceCreateEvent
def space = event.space

def input = new JsonBuilder([
    projectTypeKey    : "business",
    projectTemplateKey: "com.atlassian.jira-core-project-templates:jira-core-task-management",
    name              : space.name,
    key               : space.key,
    lead              : event.space.creator.name,
]).toString()

def request = applicationLinkRequestFactory.createRequest(POST, "/rest/api/2/project")
    .addHeader("Content-Type", "application/json")
    .setEntity(input)

request.execute(new ResponseHandler<Response>() {
    @Override
    void handle(Response response) throws ResponseException {
        if (response.statusCode != 201) {
            log.error("Creating jira project failed: ${response.responseBodyAsString}")
        }
    }
})

For how-to questions please ask on Atlassian Answers where there is a very active community. Adaptavist staff are also likely to respond there.

Ask a question about ScriptRunner for JIRA, for for Bitbucket Server, or for Confluence.