Guide · Confluence Cloud
How to export Questions for Confluence Cloud data
An exporter for Questions for Confluence in Cloud is a feature request on Atlassian's tracker, marked Future Consideration (CONFCLOUD-51723). You don't have to wait for it. Questions for Confluence stores its questions and answers as Confluence content, and the Confluence REST API can read that content. This guide shows how.
Where Questions for Confluence keeps its data
Questions for Confluence stores everything as Confluence content of its own types. This is what we found on a Confluence Cloud site:
- Questions are content of the type
ac:com.atlassian.confluence.plugins.confluence-questions:question. - Answers are content of the type
ac:com.atlassian.confluence.plugins.confluence-questions:answer. The container of an answer is its question. - Text is in
body.storage, Confluence's storage format (HTML). The author and date are inhistory.createdByandhistory.createdDate. - Topics are ordinary labels on the question.
- The accepted answer is a content property on the question,
atl_cq_accepted_answer, with a value like{"answer_id": "1376269"}.
Because it is Confluence content, the API only returns what your own account may see. Use an account that can see every space with questions.
What you need
- An API token for your Atlassian account, made at id.atlassian.com (Atlassian: Manage API tokens for your Atlassian account). Treat it like a password and revoke it when you're done.
- Basic auth: your account email as the user name and the token as the password (Atlassian: Basic auth for REST APIs).
- Python 3, only for the script below. It uses nothing outside the standard library.
Try it with curl
This asks Confluence's search for the first five questions, with their text, topics, space and accepted answer:
curl -s -u "you@example.com:$API_TOKEN" -G \
"https://your-site.atlassian.net/wiki/rest/api/content/search" \
--data-urlencode 'cql=type = "ac:com.atlassian.confluence.plugins.confluence-questions:question" order by created asc' \
--data-urlencode 'expand=body.storage,history,metadata.labels,space,metadata.properties.atl_cq_accepted_answer' \
--data-urlencode 'limit=5'
Each result is one question: id, title, space, the question in body.storage.value, the topics in metadata.labels and, when there is one, the accepted answer's ID in metadata.properties.atl_cq_accepted_answer.value.answer_id. For the answers, put the answer type in the CQL and use expand=body.storage,history,container,space; container.id is the ID of the question.
When there are more results, the response has _links.next: a link with a cursor to the next page (Atlassian: Confluence REST API, search).
Export everything with a short script
This script reads every question and answer your account can see, page by page, and writes two files: qfc-export.json with everything Confluence returned, and qfc-export.csv with one row per question and one per answer. For a question, the accepted column holds the ID of the accepted answer; for an answer, it says yes when that answer is the accepted one.
# Export Questions for Confluence Cloud: questions and answers to JSON and CSV.
# Run: SITE=your-site.atlassian.net EMAIL=you@example.com API_TOKEN=... python3 export_qfc.py
import base64, csv, json, os, urllib.parse, urllib.request
SITE = os.environ["SITE"]
AUTH = base64.b64encode(f"{os.environ['EMAIL']}:{os.environ['API_TOKEN']}".encode()).decode()
QFC = "ac:com.atlassian.confluence.plugins.confluence-questions"
def search(kind, expand):
"""All content of one Questions type, oldest first, page by page."""
params = {"cql": f'type = "{QFC}:{kind}" order by created asc', "expand": expand, "limit": 50}
url = f"https://{SITE}/wiki/rest/api/content/search?" + urllib.parse.urlencode(params)
while url:
request = urllib.request.Request(url, headers={"Authorization": f"Basic {AUTH}", "Accept": "application/json"})
with urllib.request.urlopen(request) as response:
data = json.load(response)
yield from data["results"]
links = data.get("_links", {})
url = links.get("base", f"https://{SITE}/wiki") + links["next"] if links.get("next") else None
questions = list(search("question", "body.storage,history,metadata.labels,space,metadata.properties.atl_cq_accepted_answer"))
answers = list(search("answer", "body.storage,history,container,space"))
def accepted_answer_id(question):
prop = question.get("metadata", {}).get("properties", {}).get("atl_cq_accepted_answer") or {}
return str((prop.get("value") or {}).get("answer_id", ""))
def row(item, kind, question_id, accepted):
history = item.get("history", {})
labels = item.get("metadata", {}).get("labels", {}).get("results", [])
return {
"type": kind,
"id": item["id"],
"question_id": question_id,
"space": item.get("space", {}).get("key", ""),
"title": item.get("title", ""),
"author": history.get("createdBy", {}).get("displayName", ""),
"created": history.get("createdDate", ""),
"topics": " ".join(label["name"] for label in labels),
"accepted": accepted,
"body_html": item.get("body", {}).get("storage", {}).get("value", ""),
}
answers_by_question = {}
for answer in answers:
answers_by_question.setdefault(str(answer.get("container", {}).get("id")), []).append(answer)
rows = []
for question in questions:
accepted = accepted_answer_id(question)
rows.append(row(question, "question", question["id"], accepted))
for answer in answers_by_question.get(question["id"], []):
rows.append(row(answer, "answer", question["id"], "yes" if answer["id"] == accepted else ""))
with open("qfc-export.json", "w", encoding="utf-8") as f:
json.dump({"questions": questions, "answers": answers}, f, indent=2, ensure_ascii=False)
with open("qfc-export.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["type", "id", "question_id", "space", "title", "author",
"created", "topics", "accepted", "body_html"])
writer.writeheader()
writer.writerows(rows)
print(f"{len(questions)} questions, {len(answers)} answers")
The text stays in Confluence's storage format (HTML), so anything you import elsewhere needs to handle HTML.
Moving to another app instead
Full disclosure: I build AskSpace at Dortus. If you want to move your questions rather than keep a copy, AskSpace reads Questions for Confluence on the same site directly: questions, answers, topics and accepted answers. You see a preview before anything is saved, and you can undo the import afterwards. Nothing changes in Questions for Confluence. AskSpace is listed on the Atlassian Marketplace; Getting started with AskSpace has the steps, and AskSpace vs Questions for Confluence compares the two.
Questions
Did the script not work on your site, or did we miss something? Email support@dortus.com.
AskSpace by Dortus
Questions and answers per Confluence space
Ask, answer, vote and mark one best answer, with separate rules for who may ask and who may answer. Runs on Atlassian Forge; your questions are stored by Atlassian, not on Dortus servers.
See also: Q&A in Confluence Cloud: your options