| applyTo | ** |
|---|
Use this guide whenever creating or updating Azure DevOps work items that include rich text in System.Description.
- Ensure descriptions render as Markdown (not HTML/plain text)
- Preserve newline characters and list structure
- Verify work items after every batch update
- Always use
az restfor description content-type changes. - Use
application/json-patch+jsonfor PATCH requests. - Set
multilineFieldsFormat.System.Descriptiontomarkdown. - Preserve exact newlines in the Markdown body.
- Verify both format and newline integrity after updates.
Use Azure DevOps resource audience when calling az rest:
- Resource:
499b84ac-1321-427f-aa17-267ca6975798
Example auth check:
az rest \
--method GET \
--resource 499b84ac-1321-427f-aa17-267ca6975798 \
--url "https://dev.azure.com/<org>/_apis/projects?api-version=7.1-preview.4"Note: API versions can differ by endpoint. The examples below use 7.1-preview.3 for work item PATCH calls, while this auth check uses 7.1-preview.4.
Some work items reject a direct type switch unless a valid value is provided. Use this two-step process:
az boards work-item show --id <id> | jq -j '.fields["System.Description"] // ""' > ./desc-backup.txtNOTE: Writing to a file preserves all characters including trailing newlines. Command substitution (
$(...)) silently strips trailing newlines, which would corrupt multi-line Markdown content.
PATCH_STEP1_FILE="${PATCH_STEP1_FILE:-./patch-step1.json}"
jq -n '[
{"op":"replace","path":"/fields/System.Description","value":""},
{"op":"replace","path":"/multilineFieldsFormat/System.Description","value":"markdown"}
]' >"$PATCH_STEP1_FILE"
az rest \
--method PATCH \
--resource 499b84ac-1321-427f-aa17-267ca6975798 \
--url "https://dev.azure.com/<org>/<project>/_apis/wit/workitems/<id>?api-version=7.1-preview.3" \
--headers "Content-Type=application/json-patch+json" \
--body @"$PATCH_STEP1_FILE"PATCH_STEP2_FILE="${PATCH_STEP2_FILE:-./patch-step2.json}"
jq -n --rawfile d ./desc-backup.txt '[
{"op":"replace","path":"/fields/System.Description","value":$d}
]' >"$PATCH_STEP2_FILE"
az rest \
--method PATCH \
--resource 499b84ac-1321-427f-aa17-267ca6975798 \
--url "https://dev.azure.com/<org>/<project>/_apis/wit/workitems/<id>?api-version=7.1-preview.3" \
--headers "Content-Type=application/json-patch+json" \
--body @"$PATCH_STEP2_FILE"After updates, confirm newline characters are still present and structure was not flattened.
az boards work-item show --id <id> | jq '.multilineFieldsFormat, .fields["System.Description"][0:200]'Expected:
multilineFieldsFormat.System.Description == "markdown"- Description text contains
\nwhere line breaks are expected
az boards work-item show --id <id> \
| jq -r '.fields["System.Description"]' \
| awk 'END { print NR }'If a multi-line description unexpectedly returns 1, newline content was likely lost.
Use this after bulk updates:
python3 - <<'PY'
import json, subprocess
ids = [12345, 12346] # replace with your target IDs
bad = []
for i in ids:
out = subprocess.check_output(["az", "boards", "work-item", "show", "--id", str(i)], text=True)
j = json.loads(out)
fmt = (j.get("multilineFieldsFormat") or {}).get("System.Description")
desc = j.get("fields", {}).get("System.Description") or ""
if fmt != "markdown" or "\n" not in desc:
bad.append((i, fmt, "has_newlines" if "\n" in desc else "missing_newlines"))
print("noncompliant:", len(bad))
for row in bad:
print(row)
PY401orTF400813: wrong token audience or insufficient auth contextContent-Type ... not supported: must useapplication/json-patch+jsontype changed without a value: use two-step pattern (empty + markdown type, then restore text)