diff --git a/notebooks/code_sharing/markdown_html_logging_regression.ipynb b/notebooks/code_sharing/markdown_html_logging_regression.ipynb
new file mode 100644
index 000000000..ed0b230b5
--- /dev/null
+++ b/notebooks/code_sharing/markdown_html_logging_regression.ipynb
@@ -0,0 +1,364 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "copyright-sc17911",
+ "metadata": {},
+ "source": [
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "***\n",
+ "\n",
+ "Copyright © 2023-2026 ValidMind Inc. All rights reserved.
\n",
+ "Refer to [LICENSE](https://github.com/validmind/validmind-library/blob/main/LICENSE) for details.
\n",
+ "SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "title",
+ "metadata": {},
+ "source": [
+ "# Markdown and HTML logging regression (SC-17911)\n",
+ "\n",
+ "This notebook verifies the compatibility contract for `log_text()` and `/tracking/log_metadata` after the WAF-safe TeX change. It covers plain Markdown, inline and display TeX, Markdown extensions, mixed Markdown/HTML, explicit HTML, and explicit MathJax HTML.\n",
+ "\n",
+ "The first two test matrices are deterministic and do not require credentials or a running backend:\n",
+ "\n",
+ "- When the backend advertises `log_metadata_markdown`, Markdown remains raw on the wire and carries `text_format=\"markdown\"`. The SDK does not put generated MathJax `
',\n",
+ " },\n",
+ "]\n",
+ "\n",
+ "assert {is_html(case[\"text\"]) for case in CASES} == {False, True}\n",
+ "pd.DataFrame(\n",
+ " {\n",
+ " \"case\": [case[\"name\"] for case in CASES],\n",
+ " \"detected_as\": [\"html\" if is_html(case[\"text\"]) else \"markdown\" for case in CASES],\n",
+ " }\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "capture-helper",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "async def capture_log_text_request(case, supports_markdown):\n",
+ " \"\"\"Run alog_text while capturing the serialized log_metadata body.\"\"\"\n",
+ " captured = {}\n",
+ "\n",
+ " async def capture_post(path, params=None, data=None, **kwargs):\n",
+ " captured[\"path\"] = path\n",
+ " captured[\"params\"] = params\n",
+ " captured[\"body\"] = json.loads(data)\n",
+ " return {\"content_id\": captured[\"body\"][\"content_id\"], \"text\": captured[\"body\"].get(\"text\")}\n",
+ "\n",
+ " original_flags = client_config.feature_flags\n",
+ " try:\n",
+ " client_config.feature_flags = {\"log_metadata_markdown\": supports_markdown}\n",
+ " with patch.object(api_client, \"_post\", new=capture_post):\n",
+ " await api_client.alog_text(\n",
+ " content_id=f'test_description:sc17911_{case[\"name\"]}',\n",
+ " text=case[\"text\"],\n",
+ " )\n",
+ " finally:\n",
+ " client_config.feature_flags = original_flags\n",
+ "\n",
+ " assert captured[\"path\"] == \"log_metadata\"\n",
+ " return captured[\"body\"]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "new-backend-heading",
+ "metadata": {},
+ "source": [
+ "## New backend: WAF-safe Markdown transport\n",
+ "\n",
+ "Markdown and TeX must stay raw in the request. Fully formed HTML must retain the legacy pass-through behavior."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "new-backend-matrix",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "new_backend_rows = []\n",
+ "for case in CASES:\n",
+ " body = await capture_log_text_request(case, supports_markdown=True)\n",
+ " html_input = is_html(case[\"text\"])\n",
+ "\n",
+ " assert body[\"text\"] == case[\"text\"]\n",
+ " if html_input:\n",
+ " assert \"text_format\" not in body\n",
+ " else:\n",
+ " assert body[\"text_format\"] == \"markdown\"\n",
+ " assert \"',
+ },
+ )
+
+ with patch.dict(
+ api_client.client_config.feature_flags,
+ {"log_metadata_markdown": True},
+ ):
+ self.run_async(
+ api_client.alog_text,
+ "text_woe_equation",
+ text=equation,
+ )
+
+ mock_post.assert_called_once_with(
+ f"{os.environ['VM_API_HOST']}/log_metadata",
+ data=json.dumps(
+ {
+ "content_id": "text_woe_equation",
+ "text": equation,
+ "text_format": "markdown",
+ }
+ ),
+ )
+ request_body = mock_post.call_args.kwargs["data"]
+ self.assertNotIn("