Macros are small code generators that perform a task. Script macros allow you to include dynamic content.

Create a Script Macro

Follow these steps to create a custom macro:

  1. Select the Cog icon, and then select General Configuration.

  2. Scroll to the ScriptRunner section in the left-hand navigation, and then select Script Macros.

  3. Select the Add New Item button.

  4. Select the Custom Script Macro link.

  5. Fill out the following fields:

    • Macro Code: Use to change the content of the macro.

    • Macro JavaScript Code: Use to change the behaviors of the macro.

    • Macro CSS Style: Use to change the style of the macro.

    • Output Type: Use Block or Inline to change the layout flow of the macro.

    • Body Type: Use None or Rich Text to type in the body of the macro.

If you choose Rich Text, you’ll want to change the script to the following:

"<b>" + body + "</b>"
  1. Select Add.

Browse Macro Functionality

After you click Create Macro, a search bar appears that allows you to Search ScriptRunner Functionality. Use this search bar to search available macros.

Search Bar

For example, if you’re looking for a macro that deletes labels you could type "Delete" and press Enter. Then, the list of macros is narrowed down to only those containing the word "delete" in their title or description.

Script Macros on Pages

When you open a Confluence editor (like Create a Page), you can choose your custom macro in the macro browser. You can see the new macro in the following image:

trivial macro edit
If you already had the editor open, you need to shift-refresh after editing macros definitions.

Once you save the page, it looks like the following image:

trivial macro view

For an example of a custom macro fields, the following image contains information in the fields to output a button and apply style.

trivial macro

Binding Variables

Your script has access to the following binding variables, without the need for them to be declared:

  • Parameters: A Map<String, String> for accessing user-provided macro parameters. The map’s keys are the parameter keys, so to get the color parameter use parameters.color.

  • Body: The body of the macro. The value is null if there is no body.

  • Context: A ConversionContext contains information about the current page and output device type (desktop, mobile). Use this for methods of XhtmlContent.

Including All Child Pages

You can include the content of all child pages using the Including All Child Pages macro. It is an excellent example of a common technique in macros, which is to write the XML for one or more other macros, and then pass the resulting XML through the rendering engine which converts storage XML to view format.

The following image is an example of an Including All Child Pages macro:

import com.atlassian.confluence.pages.Page
import com.atlassian.confluence.xhtml.api.MacroDefinition
import com.atlassian.confluence.xhtml.api.XhtmlContent
import com.atlassian.renderer.v2.RenderUtils
import com.atlassian.sal.api.component.ComponentLocator
import groovy.xml.MarkupBuilder

def xhtmlContent = ComponentLocator.getComponent(XhtmlContent)
def writer = new StringWriter()
def builder = new MarkupBuilder(writer)
def entity = context.getEntity()

if (entity instanceof Page) {
    builder.div('class': "child-page") {
        def content = (entity as Page).children.collect { child ->
            def macroContent = xhtmlContent.convertMacroDefinitionToStorage(MacroDefinition.builder()
                .withName("include")
                .withMacroBody(null)
                .withParameters([
                                  "0": "${context.spaceKey}:${child.title}".toString()
                ]).build(), context) (1)
            h2(child.title) (2)
            mkp.yield(xhtmlContent.convertStorageToView(macroContent, context)) (3)
        }
    }

    return xhtmlContent.convertStorageToView(writer.toString(), context)
} else {
    RenderUtils.blockError("Error", "Should only be used on pages, not comments, blog posts, etc")
}
1 This line creates the storage format XML for the Include Page macro.
2 This line creates a heading with the child page’s title.
3 This line renders the content of the Include Page macro from its storage format XML.

To create one, take the immediately descending child pages and write the Include Page macro XML. You can pick and choose sub-pages, like selecting just pages with labels.

To make this include children recursively, use descendants instead of children.

The following image is an example of another configuration form:

include children config

Lazy Loaded Macros

If you have a macro that takes more than a second or so to run, consider checking the Lazy Loaded box. The Lazy Loaded box means that the rendering of the page is not delayed until the macro has executed. Instead, the traditional "spinning gears" icon appears, and the macro content is loaded asynchronously via Representational State Transfer (REST).

For example, a macro that displays the latest version of each Atlassian product. Because it makes calls to the Atlassian Marketplace REST API, it takes a couple of seconds to complete.

The following image shows the macro form:

atl versions macro

The following image shows the macro code:

import com.atlassian.confluence.xhtml.api.XhtmlContent
import com.atlassian.sal.api.component.ComponentLocator
import groovy.xml.MarkupBuilder
import groovyx.net.http.ContentType
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.Method

def httpBuilder = new HTTPBuilder("https://marketplace.atlassian.com")
def xhtmlContent = ComponentLocator.getComponent(XhtmlContent)

def rt = httpBuilder.request(Method.GET, ContentType.JSON) {
    uri.path = "/rest/1.0/applications"
}

List<Map> apps = rt.applications

def writer = new StringWriter()
def builder = new MarkupBuilder(writer)

builder.table {
    tbody {
        tr {
            th { p("App") }
            th { p("Latest version") }
        }

        apps.sort { it.order }.each { app ->
            rt = httpBuilder.request(Method.GET, ContentType.JSON) {
                uri.path = "/rest/1.0/applications/${app.key}"
            }

            tr {
                td { p(app.name) }
                td { p(rt.versions.isEmpty() ? "No Versions Found" : rt.versions.first().version) }
            }
        }
    }
}

xhtmlContent.convertStorageToView(writer.toString(), context)

The following image shows what the macro produces:

atl versions macro view

Macros with Parameters

You can set up a macro that accepts parameters. For example, follow these steps to create a macro that outputs an AUI message.

  1. Create a new script macro by selecting the Custom Script Macro link.

  2. Set the following fields:

    • Key: message-macro

    • Name: Message

    • Description: Renders an AUI message

    • Body Type: Rich text

    • Output Type: Black

      The completed fields appear in the following image:

      macro with params 1
  1. Select the + Parameter button and set the following fields on the form that appears:

    • Parameter Type: string

    • Name: title

    • Label: Title

    • Tick the checkbox for Required

    • Parameter Type: enum

    • Name: level

    • Label: level

    • Tick the checkbox for Required

      The completed fields appear in the following image:

      macro with params 2
  1. Select the + Enum Value to add the third enum value and fill out the following fields:

    • Parameter Type: enum

    • Name: level

    • Label: level

    • Tick the checkbox for Required

Steps 3 and 4 determine the body and two arguments of the macro, which allow you to determine the following components of the message:

  • the title of the message

    • A string

  • the severity of the message (info, warning, error)

    • An enum because only a finite list of level types are accepted

  • the body of the message

    • An enum because only a finite list of level types are accepted

Find out more about the different parameter types here.

  1. For the script, you can type in the following code or put it in a file and enter the relative path to the file, under the script root (as usual).

    import groovy.xml.MarkupBuilder
    
    def writer = new StringWriter()
    def builder = new MarkupBuilder(writer)
    builder.div('class': "aui-message aui-message-${parameters.level}") {
        p('class': 'title') {
            strong(parameters.title)
        }
        mkp.yieldUnescaped(body)
    }
    writer.toString()
The mkp.yieldUnescaped method is dangerous and should only be used with trusted data. In this particular case, the use of mkp.yieldUnescaped is safe because Confluence does not allow script tags or other potentially malicious HTML in the macro’s body variable. Other user inputs like the ones you specify as macro parameters are not checked in the same way and should not be trusted. It’s imporant that these inputs are handled securely, as discussed in our documentation on custom macros and security.
  1. (Optional) Experiment with changing the parameters in the Edit 'Message' Macro dialog box that appears. This screen appears because parameters were marked as required in steps 3 and 4.

    macro with params 3

Re-Using Existing Macros

Some existing macros have many complex parameters, and you may want to get some standardisation over their usage. Script macros provide an easy way to standardise parameters.

For example, the Table of Contents macro has many parameters. Use the following task as an example of re-using existing macro parameters:

  1. In a sample page, configure the Table of Contents macro parameters like you would want your users to.

  2. View the storage format of the page.

    To view the storage format, you need the Confluence Source Editor plugin.
  1. Copy the macro XHTML from the storage format, which should look similar to the following image:

    source editor
  1. Create a new script macro with no body and no parameters.

  2. Add script to the macro, which should look similar to the following code:

    import com.atlassian.confluence.xhtml.api.XhtmlContent
    import com.atlassian.sal.api.component.ComponentLocator
    
    def xhtmlContent = ComponentLocator.getComponent(XhtmlContent)
    
    xhtmlContent.convertStorageToView("""
      <ac:structured-macro ac:name="toc" ac:schema-version="1">
        <ac:parameter ac:name="maxLevel">3</ac:parameter>
        <ac:parameter ac:name="indent">12px</ac:parameter>
        <ac:parameter ac:name="class">my-class</ac:parameter>
        <ac:parameter ac:name="printable">false</ac:parameter>
      </ac:structured-macro>
    """, context)
    You must use the convertStorageToView method, as shown in the example code, to convert from storage format to HTML.

When you finish this task, the new macro uses the Table of Contents macro parameters.

Further examples

CQL Search Macro

A CQL Search macro allows you to enter Common Query Language (CQL). The following image is a CQL Search macro form filled with example content:

cql search macro

The following image is an example of the code for this macro:

import com.atlassian.confluence.api.model.Expansion
import com.atlassian.confluence.api.model.content.Content
import com.atlassian.confluence.api.model.pagination.PageResponse
import com.atlassian.confluence.api.model.pagination.SimplePageRequest
import com.atlassian.confluence.api.model.search.SearchContext
import com.atlassian.confluence.api.service.search.CQLSearchService
import com.atlassian.confluence.xhtml.api.XhtmlContent
import com.onresolve.scriptrunner.runner.ScriptRunnerImpl
import groovy.xml.MarkupBuilder

def cqlSearchService = ScriptRunnerImpl.getOsgiService(CQLSearchService)
def xhtmlContent = ScriptRunnerImpl.getOsgiService(XhtmlContent)
def maxResults = parameters.maxResults as Integer ?: 10

def cqlQuery = parameters.cql as String

def pageRequest = new SimplePageRequest(0, maxResults)
def searchResult = cqlSearchService.searchContent(cqlQuery, SearchContext.builder().build(), pageRequest, Expansion.combine("space")) as PageResponse<Content>

def writer = new StringWriter()
def builder = new MarkupBuilder(writer)

if (searchResult.size()) {
    builder.table {
        tbody {
            tr {
                th { p("Title") }
                th { p("Space") }
            }
            searchResult.results.each { content ->
                tr {
                    td {
                        p {
                            "ac:link" {
                                "ri:page"("ri:content-title": content.title, "ri:space-key": content.space.key)
                            }
                        }
                    }
                    td { p(content.space.name) }
                }
            }
        }
    }

    if (searchResult.respondsTo("totalSize")) { // confl 5.8+
        builder.p("Showing ${Math.min(maxResults, searchResult.totalSize())} of ${searchResult.totalSize()}")
    }
} else {
    builder.p("No results")
}

def view = xhtmlContent.convertStorageToView(writer.toString(), context)
view

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.