Listeners
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 link: here.
You could respond to events, for example in order to:
-
ensure a JIRA project exists for a corresponding Bitbucket project
-
send mail when a repository is forked
-
set up Continuous Integration for new repositories
Some events in Bitbucket are cancelable - you can cancel an event in order to prevent a user:
-
creating a project
-
a repository
-
forking a project
Find the list of cancelable events here.
Adding a Listener
Navigate to Admin → Script Event Handlers. Click a heading to add a handler. Choose Custom Listenerr 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.

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. |
Usage
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.

Naming standard enforcement
Project and Repository Naming Standards Enforcement
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:

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

If you need greater control, it’s easy to write your own naming rule handler. |
Enforce branch and tag naming standards
This event handler is deprecated. Please use pre-receive hook instead.
Send Custom Email on Event
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:

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:

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:

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: - email1@company.com, email2@company.com - email1@company.com email2@company.com |
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 Mail configuration section.
A simple example of a Mail configuration section that defines config variables for the RepositoryForkedEvent
is:
config.username = $event.user.username
config.projectKey = $event.repository.origin.project.key
config.originName = $event.repository.origin.name
config.repoName = $event.repository.name
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.
Auto-Add Reviewers to Pull Requests
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.

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

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:

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

It makes sense to combine this with the pull request setting to require all tasks to be resolved. |
Auto-Merge Pull Requests on Approval
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:

Withdraw Approvals When a Pull Request Changes
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 Listener that listens for the PullRequestParticipantUnapprovedEvent
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
import com.onresolve.scriptrunner.canned.ConfiguredObjectMapper
import com.onresolve.scriptrunner.canned.bitbucket.events.BitbucketEventListenerExecutionContext
import com.onresolve.scriptrunner.canned.bitbucket.events.email.StashSendCustomEmailListener
import com.onresolve.scriptrunner.canned.bitbucket.events.model.StashSendCustomEmailListenerCommand
import com.onresolve.scriptrunner.mail.EmailFormat
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl
def pullRequest = event.pullRequest
def navBuilder = ComponentLocator.getComponent(NavBuilder)
def pullRequestUrl = navBuilder.repo(pullRequest.toRef.repository).pullRequest(pullRequest.id).buildAbsolute()
def objectMapper = ScriptRunnerImpl.scriptRunner.getBean(ConfiguredObjectMapper).get()
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 $pullRequest.fromRef.id have been changed"
def params = [
"FIELD_EMAIL_SUBJECT_TEMPLATE": subjectTemplate.toString(),
"FIELD_EMAIL_TEMPLATE" : emailTemplate.toString(),
"FIELD_TO_ADDRESSES" : event.participant.user.emailAddress, (1)
"FIELD_CC_ADDRESSES" : null,
"FIELD_EMAIL_FORMAT" : EmailFormat.HTML.name(), (2)
]
def command = objectMapper.convertValue(params, StashSendCustomEmailListenerCommand)
def executionContext = new BitbucketEventListenerExecutionContext(event: event)
ScriptRunnerImpl.scriptRunner.createBean(StashSendCustomEmailListener).execute(command, executionContext) (3)
1 | Only sends an email to the pull request participant for which the approval has been removed. |
2 | If you want to send an email with plain text content, change the emailTemplate variable and enter EmailFormat.TEXT.name() instead of EmailFormat.HTML.name() . |
3 | Uses the internal ScriptRunner script to send an email. |
Block 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:

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:

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 |
Check Delete Branch Checkbox
This script automatically check the Delete source branch after merging checkbox present on the pull request merge dialog. Automatic checking of the checkbox may be useful for teams that want to enforce that branches are deleted once they have been merged into other branches.
This script can optionally be configured to only check the checkbox if the source branch name of the pull request matches a specific pattern. A list of regular expressions can be provided in the Branch Naming Patterns field, the checkbox will only be checked for source branches with a name that matches one or more of these expressions.
For example, if you only wanted the checkbox to be checked for source branch names that begin with the text feature/
you could configure the following expression:
feature\/[\d\w-]+
Working with Custom Listeners
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)
.
event.cancel('foo')
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 |
Samples
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(event.user.name)
def directory = crowdDirectoryService.findDirectoryById(cwdUser.directoryId)
if (!(directory.name 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>() {
@Override
Page<Repository> get(PageRequest pageRequest) {
repositoryService.findByProjectKey(project.key, pageRequest) as Page<Repository>
}
}, 100).sum { Repository repo ->
repositoryService.getSize(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)
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)
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 listener that you need to apply it only to particular projects or repositories.
For example to apply the allow only 5 personal repos custom listener 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
import groovyx.net.http.ContentType
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.Method
RepositoryForkedEvent event = event
def http = new HTTPBuilder("https://hooks.slack.com")
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: *${event.repository.name}* " +
"by *${event.user.displayName}*. Come and join our chat!",
icon_emoji: ":ghost:",
mrkdwn : true,
]).toString()
}
1 | Slack will give you this value |
This script would be attached to the RepositoryForkedEvent event.
|
Update all related JIRA issues when Pull Request Opened
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.sal.api.net.Request
import com.atlassian.sal.api.net.Response
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)
def jiraIssueService = ScriptRunnerImpl.getOsgiService(JiraIssueService)
def pullRequest = event.getPullRequest()
def repository = pullRequest.fromRef.repository
assert pullRequest
def keys = jiraIssueService.getIssuesForPullRequest(repository.id, pullRequest.id)*.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}/" +
"pull-requests/${pullRequest.id}/overview"
def input = new JsonBuilder([
body: "Pull Request [${pullRequest.id}|$prUrl] has been updated:\n" +
"\n" +
"{{${pullRequest.title}}}"
]).toString()
keys.each { String key -> (2)
authenticatedRequestFactory (3)
.createRequest(Request.MethodType.POST, "rest/api/2/issue/$key/comment?expand=renderedBody")
.addHeader("Content-Type", "application/json")
.setEntity(input)
.execute([
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")
.execute([
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
-
Blocking write access to users other than the owner, for personal repositories: https://answers.atlassian.com/questions/38753103/answers/38753610
-
Creating a master branch on repository creation: https://answers.atlassian.com/questions/44219697/answers/44224550
Have questions? Visit the Atlassian Community to connect, share, and learn with other Atlassian users and experts, including Adaptavist staff.
Ask a question about ScriptRunner for JIRA, Bitbucket Server, or Confluence.
Want to learn more? Check out courses on Adaptavist Learn, an online platform to onboard and train new users for Atlassian solutions.