AI Reference: Create a Confluence Page with a Diagram
This document tells an AI assistant how to create a Confluence page that already contains a rendered Mermaid diagram, using the Confluence REST API only. No one has to open the page and edit the macro afterwards - the diagram is part of the page from the moment it is created.
Use this document as context when asking an AI assistant to build a page:
Here is the guide: https://docs.apportunity.xyz/mermaid-diagrams/create-confluence-page-ai-prompt. Create a Confluence page that explains [your topic] with a diagram.
For a plain-language explanation of what this enables, who it is for, and ready-made prompts to copy, see Create Pages with AI.
For help choosing a diagram type and writing correct Mermaid syntax, use the companion page: AI Reference.
How It Worksβ
A Confluence page body is a JSON document (ADF, "Atlassian Document Format"). A macro appears in that document as an extension node. Macro configuration is stored on that node under attrs.parameters.guestParams.
The Mermaid Diagrams app reads guestParams whenever the macro has no diagram stored of its own. So putting the Mermaid source there means the macro renders it on first view.
The page source stays the source of truth. Viewing a page never stores anything, so you can rewrite guestParams as many times as you like and every reader sees the new version. This is what makes a page you generated safe to regenerate.
That changes the first time somebody edits the diagram in Confluence. At that point the app saves the diagram as a page attachment, along with a PNG preview used for PDF and Word export, and the attachment takes over. From then on guestParams is ignored.
Before the first edit, the page source wins and you can rewrite it freely.
After the first edit, the stored diagram wins and rewriting guestParams does nothing. Change it in the Confluence UI instead.
Export uses a preview image, which the app renders the first time somebody with edit
access opens the page. It refreshes automatically when you rewrite guestParams, so
exports keep pace with the page source. Nothing is required of you.
Prerequisitesβ
- The Apportunity: Mermaid Diagrams Macro for Confluence app is installed on the site.
- A Confluence Cloud API token. Create one at id.atlassian.com/manage-profile/security/api-tokens.
- Permission to create pages in the target space.
All requests use HTTP Basic auth with your account email and the API token.
export SITE="https://your-site.atlassian.net"
export EMAIL="you@example.com"
export API_TOKEN="your-api-token"
Step 1: The Macro's extensionKeyβ
extensionKey tells Confluence which app the macro belongs to. It is the same on every Confluence site, so use this value directly:
9d10a082-4d4e-4efa-996b-9c0170f3bfee/7a286cf2-9d8a-4c57-9bd2-102c413343b2/static/mermaid-macro
The two UUIDs are the app's ID and its production environment ID. Both are properties of the app itself, not of your site, so they do not change between customers and do not need to be looked up.
If the macro does not render, or you are on a non-production install
Sites running a development or staging build of the app use a different environment ID. To read the correct value from a site, find a page that already uses the macro:
curl -s -u "$EMAIL:$API_TOKEN" \
--get "$SITE/wiki/rest/api/search" \
--data-urlencode 'cql=macro = "mermaid-macro"' \
--data-urlencode 'limit=1'
Take results[0].content.id, then read that page's body and copy the extensionKey of the Mermaid extension node:
curl -s -u "$EMAIL:$API_TOKEN" \
"$SITE/wiki/api/v2/pages/<PAGE_ID>?body-format=atlas_doc_format"
The body arrives as a JSON string in body.atlas_doc_format.value, so parse it before searching for the node.
import json, requests
hits = requests.get(f"{SITE}/wiki/rest/api/search",
params={"cql": 'macro = "mermaid-macro"', "limit": 1}, auth=(EMAIL, TOKEN)).json()
page_id = hits["results"][0]["content"]["id"]
page = requests.get(f"{SITE}/wiki/api/v2/pages/{page_id}",
params={"body-format": "atlas_doc_format"}, auth=(EMAIL, TOKEN)).json()
doc = json.loads(page["body"]["atlas_doc_format"]["value"])
def find_key(node):
if isinstance(node, dict):
key = node.get("attrs", {}).get("extensionKey", "")
if node.get("type") == "extension" and key.endswith("/static/mermaid-macro"):
return key
for value in node.values():
if (found := find_key(value)):
return found
elif isinstance(node, list):
for item in node:
if (found := find_key(item)):
return found
return None
print(find_key(doc))
If the search returns nothing, no page on that site uses the macro yet. Insert one Mermaid Diagram macro on any page once (Insert + β Mermaid Diagram), then repeat.
Step 2: Find the Space IDβ
The page creation endpoint needs the numeric space ID, not the space key.
curl -s -u "$EMAIL:$API_TOKEN" "$SITE/wiki/api/v2/spaces?keys=DOCS"
Use results[0].id from the response.
Step 3: Build the Macro Nodeβ
This is the node to insert into the page body. Everything shown is required.
{
"type": "extension",
"attrs": {
"layout": "default",
"extensionType": "com.atlassian.ecosystem",
"extensionKey": "9d10a082-4d4e-4efa-996b-9c0170f3bfee/7a286cf2-9d8a-4c57-9bd2-102c413343b2/static/mermaid-macro",
"localId": "d7c1f0a2-6b4e-4a1b-9c3d-2f8e5a7b1c40",
"parameters": {
"localId": "d7c1f0a2-6b4e-4a1b-9c3d-2f8e5a7b1c40",
"guestParams": {
"diagram": "graph TD;\n A[Start] --> B[End];",
"size": "medium"
}
}
}
}
Rules for localId:
- Generate a fresh UUID v4 for every macro.
- Use the same value in
attrs.localIdandparameters.localId. - Never reuse a UUID across two macros. Two macros sharing one
localIdwill share one stored diagram and overwrite each other.
guestParams Referenceβ
Two keys. One diagram per macro.
| Key | Type | Description |
|---|---|---|
diagram | string | The Mermaid source. Required. |
size | string | Display height. One of small, medium, large, xlarge, full. Optional, defaults to medium. |
"guestParams": {
"diagram": "graph TD;\n A[Start] --> B[End];",
"size": "medium"
}
For several diagrams on a page, use several macros, each with its own diagram and its own localId. That reads better anyway, because each diagram gets its own introduction.
If the payload cannot be understood the macro shows its normal empty state rather than an error, so a malformed seed is easy to miss. Always open the page and check.
Step 4: Create the Pageβ
POST $SITE/wiki/api/v2/pages
{
"spaceId": "3932181",
"status": "current",
"title": "Checkout flow",
"body": {
"representation": "atlas_doc_format",
"value": "<the ADF document, serialised to a JSON string>"
}
}
body.value is a string, not an object. The ADF document has to be serialised into it.
The Mermaid source sits inside the ADF document, and the ADF document is itself serialised into body.value. Construct the document as a real object and let json.dumps / JSON.stringify handle the escaping. Hand-writing the backslashes is the most common way this fails.
Complete Exampleβ
This creates a page with a heading, an explanation, a flowchart, and a second diagram in its own macro.
import json, uuid, requests
SITE, EMAIL, TOKEN = "https://your-site.atlassian.net", "you@example.com", "your-api-token"
SPACE_ID = "3932181"
EXTENSION_KEY = "9d10a082-4d4e-4efa-996b-9c0170f3bfee/7a286cf2-9d8a-4c57-9bd2-102c413343b2/static/mermaid-macro"
def macro(guest_params):
local_id = str(uuid.uuid4())
return {
"type": "extension",
"attrs": {
"layout": "default",
"extensionType": "com.atlassian.ecosystem",
"extensionKey": EXTENSION_KEY,
"localId": local_id,
"parameters": {"localId": local_id, "guestParams": guest_params},
},
}
def heading(text, level=2):
return {"type": "heading", "attrs": {"level": level},
"content": [{"type": "text", "text": text}]}
def paragraph(text):
return {"type": "paragraph", "content": [{"type": "text", "text": text}]}
doc = {
"type": "doc",
"version": 1,
"content": [
heading("Checkout flow"),
paragraph("The diagram below shows how an order moves from cart to confirmation."),
macro({
"diagram": "graph TD;\n"
" A[Cart] --> B{Payment OK?};\n"
" B -- Yes --> C[Confirm order];\n"
" B -- No --> D[Show error];",
"size": "large",
}),
paragraph("Payment authorisation itself involves three services:"),
macro({
"diagram": "sequenceDiagram\n"
" Shop->>Gateway: authorise\n"
" Gateway->>Bank: verify\n"
" Bank-->>Gateway: ok\n"
" Gateway-->>Shop: approved",
"size": "medium",
}),
],
}
response = requests.post(
f"{SITE}/wiki/api/v2/pages",
auth=(EMAIL, TOKEN),
headers={"Content-Type": "application/json"},
json={
"spaceId": SPACE_ID,
"status": "current",
"title": "Checkout flow",
"body": {"representation": "atlas_doc_format", "value": json.dumps(doc)},
},
)
response.raise_for_status()
page = response.json()
print(f"{SITE}/wiki{page['_links']['webui']}")
Open the printed URL to confirm both diagrams render.
Using Confluence Storage Format Insteadβ
Some tools create pages with Confluence Storage Format (XHTML) rather than ADF. The Apportunity Secure MCP Server's confluence_create_page tool is one of them. A seeded macro works there too, with different key names.
Storage format uses kebab-case: extensionType becomes extension-type, localId becomes local-id, and guestParams becomes guest-params. Nested values are nested <ac:adf-parameter> elements.
<h2>Checkout flow</h2>
<p>The diagram below shows how an order moves from cart to confirmation.</p>
<ac:adf-extension><ac:adf-node type="extension"><ac:adf-attribute key="extension-type">com.atlassian.ecosystem</ac:adf-attribute><ac:adf-attribute key="extension-key">9d10a082-4d4e-4efa-996b-9c0170f3bfee/7a286cf2-9d8a-4c57-9bd2-102c413343b2/static/mermaid-macro</ac:adf-attribute><ac:adf-attribute key="local-id">d7c1f0a2-6b4e-4a1b-9c3d-2f8e5a7b1c40</ac:adf-attribute><ac:adf-attribute key="parameters"><ac:adf-parameter key="local-id">d7c1f0a2-6b4e-4a1b-9c3d-2f8e5a7b1c40</ac:adf-parameter><ac:adf-parameter key="guest-params"><ac:adf-parameter key="diagram">graph TD;
A[Cart] --> B{Payment OK?};
B -- Yes --> C[Confirm order];
B -- No --> D[Show error];</ac:adf-parameter><ac:adf-parameter key="size">large</ac:adf-parameter></ac:adf-parameter></ac:adf-attribute></ac:adf-node></ac:adf-extension>
Notes:
- XML-escape the Mermaid source.
>becomes>,<becomes<,&becomes&. Arrows like-->therefore appear as-->. Newlines stay as real line breaks. - The
<ac:adf-extension>element must not be nested inside a<p>. - Confluence adds an
<ac:adf-fallback>copy of the node when it saves the page. You do not need to write one. - Everything else in this guide still applies, including the
extensionKeyfrom Step 1 and thelocalIdrules.
Writing Good Pagesβ
The macro is a block element, so treat it like a figure in a document.
- Introduce each diagram with a sentence or two before it. A diagram with no explanation is hard to act on.
- One macro per diagram. Several diagrams means several macros, each with its own introduction and its own
localId. - Prefer
largefor flowcharts and sequence diagrams with more than about ten nodes,mediumfor small ones. - Keep each Mermaid source under roughly 10-20 KB. The source travels inside the page body, so very large diagrams make the page heavier for every reader.
- Split a diagram that is getting too dense into two simpler ones rather than growing it.
For diagram type selection and syntax, follow the AI Reference.
Changing a Page You Already Createdβ
To change a diagram you generated, rewrite the macro's guestParams in the page body. This works for as long as nobody has edited that diagram in Confluence.
Read the page, change the node (or insert a new one), and write it back with the version number incremented.
GET $SITE/wiki/api/v2/pages/<PAGE_ID>?body-format=atlas_doc_format
PUT $SITE/wiki/api/v2/pages/<PAGE_ID>
The PUT body needs id, status, title, body, and version.number set to the current version plus one.
Two cautions:
- This creates a new page version and notifies everyone watching the page. There is no way to mark it a minor edit through this API.
- If somebody has an unpublished draft of the page, Confluence merges your change into it and a significantly diverged draft can be overwritten. Avoid rewriting pages that are being actively edited.
Troubleshootingβ
| Symptom | Cause |
|---|---|
| Macro shows "app not found" or stays blank grey | Wrong extensionKey, or the app is not installed on the site. If the site runs a development build of the app, read the key off an existing macro as described in Step 1. |
| Macro renders but shows the empty state | guestParams was not understood. Check that diagram is present and is a plain string. |
| Diagram shows an error message | The Mermaid syntax is invalid. Check it against the AI Reference. |
400 Bad Request on page creation | body.value was sent as an object. It must be a serialised JSON string. |
| Two macros show the same diagram | They share a localId. Give every macro its own fresh UUID. |
Rewriting guestParams changes nothing | Somebody has edited that diagram in Confluence, so the stored version now wins. Change it in the Confluence UI instead. |
| Only one diagram appears where you expected several | Each macro shows one diagram. Add one macro per diagram. |
| Diagram is blank in a PDF or Word export | The preview image is rendered when somebody with edit access opens the page. Open the page once as such a user, then export. |