ScriptRunner allows you to respond to Bitbucket events, via an inline script or pointer to a file. You can view the full list of events here.

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

Some events in Bitbucket are cancelable - you can cancel an event in order to prevent a user:

Find the list of cancelable events here.

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. The event types are grouped into Cancelable and Non-cancelable categories.

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

add event handler

Built-in Event Handlers

Default Project / Repository Permissions

By default, when you create a project or repository only you will have access to it. You may decide that you want:

  • default read permission to encourage collaboration

  • public access permission for repositories in particular projects

  • certain groups or users to have automatic admin access

These event handlers will listen for projects/repositories being created, and set users and groups with the specified access levels, whether they are publicly accessible or not, and for projects, set the default level of access for logged-in users.

You can use the bulk editing feature to update all existing projects to set your base level of permissions.


Navigate to Admin → Event Handlers, and click either Default project permissions or Default repository permissions. Specify whether publicly accessible or not, and the default users and groups for each permission. You can leave any that you don’t want to set blank.

You may configure multiple of each type of event handler, using conditions to apply different base permissions for different circumstances, for instance depending on the parent project, or the user creating the repository or project.

The following example shows setting the default permission of new projects to Read, and adding a group authorised-devs, and a user - Mr A N User as Administrators.

default project perms

Naming standard enforcement

Enforce project and repository naming standards

This built-in event handler allows you to easily enforce naming standards for projects and repositories. Define a regular expression for either or both project and repository names. For example:

  • enforce project names are at least ten characters, and begin with an upper-case character

  • ensure repository names are all characters in the range [a-z] you might use:

naming rule setup

When a project is created that doesn’t conform to the rules, it will be blocked like this:

project blocked
If you need greater control, it’s easy to write your own naming rule handler.

Enforce branch and tag naming standards

Unlike the pre-receive hook this handles the case where the user creates a branch or tag through the Bitbucket user interface.

This allows you to enforce a naming standard for branches and tags using regular expressions.

You may want different naming standards for different types of branches. For example for your feature and bugfix branches, you may want them to contain a JIRA issue ID. For hotfix branches you may require that they begin with a Change Request ID. Therefore you can specify multiple regular expressions for both branches and tags. If none of them match, the supplied error message (or a default one), will be returned to the user.

An example where feature branches must start with a JIRA issue key, hotfix branches must begin with a CR number:


To enforce tag names follow the format: 3.2.14:


When a branch is created through the UI that doesn’t conform to the rules, it will be blocked like this:

create branch blocked
Any custom message added for enforcing tag names will not be displayed in the UI due to the way Bitbucket handles certain cancellable events.

Send mail

With this built-in event handler, your Bitbucket instance will send email notifications automatically after one or more events has been triggered. You will just need to configure which events should this handler be listening to and then provide a subject and an email template, as well as the email format (plain text or HTML) and a list of recipients. There is also a conditions field, which can give more control over this functionality.

You can either create your own email templates or use one of the following samples that are included in the default installation of ScriptRunner:

Repository forked

It provides information about the repository that has just been created and the users that performed that fork. This is how the configuration of the event handler would look like:

mail event handler fork setup

As a result of that configuration, an email will be sent to the selected email addresses with a content similar to the following one:

User admin has forked the repository PROJECT_1/rep_1.
The name of the new repository is: PROJECT_1/myNewFork

Global permissions modified

If you would like to get notified every time a change is done in the Global permissions settings, you can use the following template:

mail event handler permissions setup

An email like the following one will be sent over right after the permissions for a user have been modified:

User admin has modified the permissions for user user.
The highest global permission for this user is now: SYS_ADMIN

New task created in pull request

Users can also get notifications whenever a new task is created just by creating an event handler similar to the following one:

mail event handler tasks setup
In the image above, the event should be TaskCreatedEvent

If a new task is created, then the email addresses configured in the event handler will receive an email with a content similar to this:

User admin has just created the following task: Copy content from branch FOO-123.
Please take a look at this pull request for more information: a modification on branch basic_branching
The content of the "to addresses" and "cc addresses" should be a list of emails separated by commas or spaces, being the following two examples correct: -, -

Additional Configuration in Emails

You may notice the syntax for getting content in the template is a bit clunky, as the template engine does not allow you to use the import keyword. Rather than doing this, you can pass in a config map to the bindings for both the subject and body templates.

This is done in the Condition and Configuration section. Note that as this is used for the condition you must continue to return a boolean value to tell the system whether to send the email or not.

A simple example of a Condition and Configuration section that defines config variables for the RepositoryForkedEvent is:

config.username = $event.user.username
config.projectKey = $event.repository.origin.project.key
config.originName = $
config.repoName = $


The subject template:

User $config.username has forked a repository

The body template:

User $config.username has forked the repository $config.projectKey/$config.originName.
The name of the new repository is: $config.projectKey/$config.repoName.

As a result of that configuration, an email will be sent to the selected email addresses with content similar to the following one:

User admin has forked the repository PROJECT_1/rep_1.
The name of the new repository is: PROJECT_1/myNewFork.

If the condition is going to return false, the config variables are irrelevant. So you can return false early…​

def condition = repository.slug == "rep_1" && ...
if (! condition) return false

def expensiveConfigVars = [foo: "bar"]

config.putAll (expensiveConfigVars)

Auto Add Approvers

This will automatically add some set of reviewers when a pull request is created. You would combine this with a condition so that for example:

  • pull requests from an outsourced team automatically include a senior reviewer from the onsite team

  • the mentors of new team members are automatically added as reviewers

You can add entire groups as well as, or instead of, named users. However, only those users that are active and are have the necessary permissions in the repository will be added.

Reviewers are split into two categories:

  • default reviewers - these reviewers are automatically added to the dialog when the pull request is created, but the pull request author can remove them

  • mandatory reviewers - these are shown with a padlock in the pull request create and edit dialogs, and cannot be removed

Use mandatory reviewers to enforce that certain users to get to review changes to important or sensitive code, or branches.

In the following example, a single user and the group authorised_devs, will be added as default reviewers for all pull requests (all because the condition is blank), in the selected repository.

auto add to review

Mr Senior Dev is added as a mandatory reviewer, and cannot be removed:

auto add to review create
From Bitbucket 4.8 there is a feature that allows you to assign default reviewers for pull requests: BSERV-2924. These features can work alongside each other without interfering with each other, with one notable caveat. If you add default reviewers using the Bitbucket feature and set a certain number of them to be required, then reviewers added using the ScriptRunner event handler will not be able to approve or reject a pull request until one of the Bitbucket reviewers has weighed in. This is consistent with how Bitbucket Server’s default reviewers feature normally behaves. In short, the reviewers that are required to weigh in by Bitbucket Server settings will have the first say on a pull request.

Add tasks to new pull requests

This handler will add a default list of tasks to new pull requests where the condition matches. This is useful when you want to remind developers of some common administrative tasks that people sometimes forget to do, for instance update the documentation, or ensure there are unit tests for new code.

Tasks have to be attached to something - currently they can only be attached to a comment. So you also need to provide a comment, for instance Please remember to complete the following tasks.

The following configuration:

add tasks config

will, when a pull request is created in any configured repository, result in:

add tasks result
It makes sense to combine this with the pull request setting to require all tasks to be resolved.

Auto Merge Pull Request

This handler will automatically merge the pull request when the condition provided evaluates to true. It will only do this if can be merged with no conflicts, and no merge checks veto the merge. In other words, if it can be merged from the Bitbucket UI, it will do the merge.

The examples contain some sample conditions, such as:

  • A senior developer has approved

  • At least two reviewers have approved

There is also the option to automatically delete the branch after the pull request has been merged successfully.

A pull request can be automatically merged when every reviewer has approved it, by creating an event handler similar to the following one:

auto merge event handler

Withdraw approvals when a pull request changed

By default, a user can push more content to a pull request even after it has been approved, yet the approvals remain. This can make a mockery of your defined approval process. This handler will remove any approvals when new content is pushed to an existing pull request, if the condition matches. Using the conditions, you could apply this only when the pull request contains some sensitive code, or for less trusted developers. The default behaviour in Bitbucket is that approvers will NOT be automatically notified in the event of a changed Pull Request. In this scenario, we recommend creating an entirely new Event Handler that listens for the PullRequestUnapprovedEvent and for example sends an email to all Reviewers that had already approved the Pull Request. The following code shows a simple way to send an email after an approval has been removed:

import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.nav.NavBuilder

def pullRequest = event.pullRequest
def navBuilder = ComponentLocator.getComponent(NavBuilder)
def pullRequestUrl = navBuilder.repo(pullRequest.toRef.repository).pullRequest(

def subjectTemplate = "Your approval of the Pull Request $pullRequest.title has been removed"
def emailTemplate = "Your approval of the pull request <a href='$pullRequestUrl'>$pullRequest.title</a> has been removed," +
    "as the contents of the branch $ have been changed"

new StashSendCustomEmailListener().doScript([                               (1)
    "FIELD_EMAIL_SUBJECT_TEMPLATE"  : subjectTemplate,
    "FIELD_EMAIL_TEMPLATE"          : emailTemplate,
    "FIELD_TO_ADDRESSES"            : event.participant.user.emailAddress,  (2)
    "FIELD_CC_ADDRESSES"            : null,
    "FIELD_EMAIL_FORMAT"            : "HTML",  (3)
1 Uses the internal Script Runner canned script to send an email.
2 Only sends an email to the pull request participant for which the approval has been removed.
3 If you want to send an email with plain text content, change the emailTemplate variable and enter "Plain Text" instead of "HTML".

Block creation of out of date pull requests

By out of date, we mean a pull request that is based on an earlier version of the target branch. The problems with this are:

  • there may be conflicts, in which case it would not be mergeable

  • when the merge happens, the result is unknown - the merge result has not been built on either the topic or target branch

This will inevitably happen as changes are pushed or merged on to the target branch (for instance master). But there is no reason why the pull request can’t be created from the tip of master…​ this will give reviewers the best chance of being able to merge without conflicts, and a green build from the topic branch will mean you will get a green build after merging (as the inputs are the same).

We are trying to avoid this situation:

out of date topic branch

If a pull request is created from topic now, the merge result is unknown, and it may be conflicted. If this event handler is applied, you will see the message:

pull request create blocked

There is no need to lose any additional information that has been added in the description. In a command prompt window, rebase or merge your topic branch:

git checkout topic
git rebase master
git push -f

Alternatively to merge rather than rebase:

git checkout topic
git merge master
git push

Once done, press the Create button again.

It’s possible that the developer will need to resolve conflicts when rebasing or merging the branch, however they were going to have to be resolved sooner or later, so better to do it sooner whilst it’s fresh in their mind.

Another way of ensuring that the merge will produce a green build, is to build from the automatic merge that Bitbucket will do when a pull request is rescoped (i.e. when either branch gets new commits):

git fetch origin  +refs/pull-requests/{id}/*:refs/remotes/origin/pull-requests/{id}/*

where {id} is the ID of the pull request in question. If there is no ref pull-requests/{id}/merge it means there were conflicts. There is interesting further reading here.

Working with Custom Event Handlers

The event is contained in the script binding. For cancelable events this will implement com.atlassian.bitbucket.event.CancelableEvent.

If it is cancelable, you can prevent the operation proceeding by calling cancel on it.

There is a shorter version supplied if you do not want to worry about supplying a translated message to the user: cancel(String message).


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.

For example, in a listeners that handles project modification and project creation, getting the project key depends on whether the project is being updated or modified:

ApplicationEvent event = event (1)
def projectKey

if (event instanceof ProjectCreationRequestedEvent) {
    projectKey = event.getProject().getKey()
else if (event instanceof ProjectModificationRequestedEvent) {
    projectKey = event.getNewValue().getKey()
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


Controlling personal repository creation

To use, go to Admin → Script Event Handlers. In the Events field, choose either or both of RepositoryCreationRequestedEvent and RepositoryForkRequestedEvent. One or the other (not both) will be fired depending on whether it’s a new personal repo, or a fork into the user’s personal project.

Allow only 5 personal repos
package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal

import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.project.ProjectType
import com.atlassian.bitbucket.repository.RepositoryService

// only allow N personal
def repositoryService = ComponentLocator.getComponent(RepositoryService)
final Integer MAX_PERSONAL = 5
def event = event as RepositoryCreationRequestedEvent
def project = event.repository.project

if (project.type == ProjectType.PERSONAL) {
    if (repositoryService.countByProject(project) >= MAX_PERSONAL) {
        event.cancel("You can only create $MAX_PERSONAL personal repositories.")
User in named directory

Allow personal repo creation only if the user is in a certain named directory:

package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal

import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent
import com.atlassian.crowd.embedded.api.CrowdDirectoryService
import com.atlassian.crowd.embedded.api.CrowdService
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.project.ProjectType

def crowdService = ComponentLocator.getComponent(CrowdService)
def crowdDirectoryService = ComponentLocator.getComponent(CrowdDirectoryService)

def event = event as RepositoryCreationRequestedEvent
def project = event.repository.project

// only allow when user comes from dir X
if (project.type == ProjectType.PERSONAL && event.user) {

    def cwdUser = crowdService.getUser(
    def directory = crowdDirectoryService.findDirectoryById(cwdUser.directoryId)

    if (! ( in ["Bitbucket Internal Directory", "Stash Internal Directory"])) { // enter directory name
        event.cancel("You don't have permissions to create a personal repository.")
Allow 1 Gb of personal repository space
package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal

import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.project.ProjectType
import com.atlassian.bitbucket.repository.Repository
import com.atlassian.bitbucket.repository.RepositoryService
import com.atlassian.bitbucket.util.Page
import com.atlassian.bitbucket.util.PageProvider
import com.atlassian.bitbucket.util.PageRequest
import com.atlassian.bitbucket.util.PagedIterable

// only allow N personal
def repositoryService = ComponentLocator.getComponent(RepositoryService)
def event = event as RepositoryCreationRequestedEvent
def project = event.repository.project

// allow only 1Gb total size per user
if (project.type == ProjectType.PERSONAL) {
    def totalUserSize = new PagedIterable<Repository>(new PageProvider<Repository>() {
        Page<Repository> get(PageRequest pageRequest) {
            repositoryService.findByProjectKey(project.key, pageRequest) as Page<Repository>
    }, 100).sum { Repository repo ->

    if (totalUserSize >= 1024e3) {
        event.cancel("You can only use 1Gb in personal repository space, which you have exceeded.")
Only allow from groups

Allow only members of certain groups to create personal repos:

package com.onresolve.bitbucket.groovy.test.listeners.STASH_3850_block_personal

import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.project.ProjectType
import com.atlassian.bitbucket.user.UserService

def userService = ComponentLocator.getComponent(UserService.class)

def event = event as RepositoryCreationRequestedEvent
def project = event.repository.project

if (project.type == ProjectType.PERSONAL) {
    if (! userService.isUserInGroup(event.getUser(), "personal-project-creators")) {
        event.cancel("You don't have permissions to create a personal repository.")

Disallowing project creation except for members of a group

import com.atlassian.sal.api.component.ComponentLocator (1)
import com.atlassian.bitbucket.user.UserService

def userService = ComponentLocator.getComponent(UserService.class)

if (!userService.isUserInGroup(event.getUser(), "project-creators")) {
    event.cancel("Only users in the project-creators group can create projects")
1 imports will generally not be displayed, for clarity
This script would be attached to the ProjectCreationRequestedEvent event.

Applying event handlers to specific projects/repositories

You may find in a custom event handler that you need to apply it only to particular projects or repositories.

For example to apply the allow only 5 personal repos custom event handler to a specific project you can use the example below.

package examples.bitbucket.handler

import com.atlassian.bitbucket.event.repository.RepositoryCreationRequestedEvent
import com.atlassian.bitbucket.repository.RepositoryService
import com.atlassian.sal.api.component.ComponentLocator

def repositoryService = ComponentLocator.getComponent(RepositoryService)
final Integer MAX_PER_TEST = 5
def event = event as RepositoryCreationRequestedEvent
def project = event.repository.project

if (project.key == "TEST") {
    if (repositoryService.countByProject(project) >= MAX_PER_TEST) {
        event.cancel("You can only create $MAX_PER_TEST repositories in test projects.")

You could also apply it to a particular set of projects:

if (project.key in ["PTEST", "PTEST1", "PTEST2"]) {

If you want to apply the event handler to specific repositories instead you can use the following example below:

if (event.repository.project.key == "TEST" && event.repository.slug in [ "one", "two", "three" ]) {
If applying to specific repositories you need to check both the project key and repository slug as the repository slug is only unique to a project.

Post to Slack when a repository is forked

The same principle can be used to integrate with anything that provides a web service, as an example we use Slack.

Typically you would add this code to a non-canceleable event, so that the other system gets notified only if the operation is successful.

package examples.bitbucket.handler

import com.atlassian.bitbucket.event.repository.RepositoryForkedEvent
import groovy.json.JsonBuilder

RepositoryForkedEvent event = event

def http = new HTTPBuilder("")
http.request(Method.POST, ContentType.TEXT) {
    uri.path = "/services/XXXX/YYYYY/ZZZZZ" (1)
    body = new JsonBuilder([
        channel   : "#dev",
        username  : "webhookbot",
        text      : "New fork created on repo: *${}* " +
            "by *${event.user.displayName}*. Come and join our chat!",
        icon_emoji: ":ghost:",
        mrkdwn    : true,
1 Slack will give you this value
This script would be attached to the RepositoryForkedEvent event.

This sample will update linked issues on the creation of a Pull Request. This may be useful for informing stakeholders on progress.

package examples.bitbucket.handler

import com.atlassian.applinks.api.ApplicationLinkResponseHandler
import com.atlassian.plugin.PluginAccessor
import com.atlassian.sal.api.UrlMode
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.bitbucket.event.pull.PullRequestOpenedEvent
import com.atlassian.bitbucket.integration.jira.JiraIssueService
import com.onresolve.scriptrunner.canned.bitbucket.util.BitbucketBaseScript
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl
import groovy.json.JsonBuilder
import groovy.transform.BaseScript

@BaseScript BitbucketBaseScript baseScript

PullRequestOpenedEvent event = event

def pluginAccessor = ComponentLocator.getComponent(PluginAccessor.class)
def jiraIssueService = pluginAccessor.getEnabledPluginModule(ScriptRunnerImpl.PLUGIN_KEY + ":jiraIssueService").getModule() as JiraIssueService

def pullRequest = event.getPullRequest()
def repository = pullRequest.fromRef.repository
assert pullRequest

def keys = jiraIssueService.getIssuesForPullRequest(,*.key (1)

def jiraLink = getJiraAppLink()
def authenticatedRequestFactory = jiraLink.createImpersonatingAuthenticatedRequestFactory()

// work out the URL to the pull request
def prUrl = "${getBaseUrl(UrlMode.ABSOLUTE)}/projects/" +
    "${repository.project.key}/repos/${repository.slug}/" +

def input = new JsonBuilder([
    body: "Pull Request [${}|$prUrl] has been updated:\n" +
        "\n" +

keys.each { String key -> (2)
    authenticatedRequestFactory (3)
        .createRequest(Request.MethodType.POST, "rest/api/2/issue/$key/comment?expand=renderedBody")
        .addHeader("Content-Type", "application/json")
            handle: { Response response ->
                if (response.successful) {
                    log.debug "Created comment on issue: $key."
                } else {
                    log.warn "Failed to create comment: $response.responseBodyAsStream"
        }] as ApplicationLinkResponseHandler<Void>
1 get all issue keys related to this PR
2 iterate over the issues
3 create a comment on each issue using the JIRA REST API
For this to work you must have a working application link to JIRA set up.

Block Forking Unless Unreleased Versions

Another contrived example that will block repository forking unless an unreleased JIRA version exists in the project. This script should be attached to the RepositoryForkRequestedEvent event.

@BaseScript BitbucketBaseScript baseScript
RepositoryForkRequestedEvent event = event

def jiraLink = getJiraAppLink()
def jiraVersions = jiraLink.createImpersonatingAuthenticatedRequestFactory()
    .createRequest(Request.MethodType.GET, "rest/api/2/project/SD/versions") (1)
    .addHeader("Content-Type", "application/json")
        handle: {Response response ->
            new JsonSlurper().parse(response.responseBodyAsStream)
    ] as ApplicationLinkResponseHandler<String>

if (! (jiraVersions.any {! it.released})) { (2)
    def msg = "All JIRA versions are released for this project... " +
        "please create a new version before forking. Current versions are: ${jiraVersions*.name.join(",")}"
    event.cancel(new KeyedMessage("some.key", msg, msg))
1 Get list of versions. Note: project SD is hard-coded here
2 Look for unreleased versions

Further Examples

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.