{
  "nbformat": 4,
  "nbformat_minor": 5,
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python",
      "version": "3.13.0"
    },
    "blog_metadata": {
      "topic": "Building a Proactive Azure Ops Copilot for Mission-Critical Environments",
      "slug": "building-a-proactive-azure-ops-copilot-for-mission-critical-",
      "generated_by": "LinkedIn Post Generator + Azure OpenAI",
      "generated_at": "2026-07-10T12:27:51.780Z"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Building a Proactive Azure Ops Copilot for Mission-Critical Environments\n",
        "\n",
        "This notebook turns the blog post into a hands-on validation flow for designing a governed Azure operations copilot. The focus is evidence-first incident handling, approval-gated remediation, and least-privilege automation rather than freeform autonomous fixes in production.\n",
        "\n",
        "You will validate the architecture, simulate grounded evidence packaging, test policy decisions, and inspect deployable infrastructure and workflow artifacts."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "%pip install -q azure-identity azure-monitor-query azure-mgmt-resourcegraph pyyaml pandas"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "import json\n",
        "import os\n",
        "from datetime import datetime, timedelta, timezone\n",
        "from textwrap import dedent\n",
        "\n",
        "import yaml\n",
        "import pandas as pd\n",
        "\n",
        "try:\n",
        "    from azure.identity import DefaultAzureCredential\n",
        "    from azure.monitor.query import LogsQueryClient\n",
        "    from azure.mgmt.resourcegraph import ResourceGraphClient\n",
        "    from azure.mgmt.resourcegraph.models import QueryRequest\n",
        "    AZURE_SDK_AVAILABLE = True\n",
        "except Exception as e:\n",
        "    AZURE_SDK_AVAILABLE = False\n",
        "    AZURE_IMPORT_ERROR = str(e)\n",
        "\n",
        "print({\"azure_sdk_available\": AZURE_SDK_AVAILABLE})\n",
        "if not AZURE_SDK_AVAILABLE:\n",
        "    print(\"Azure SDK imports unavailable:\", AZURE_IMPORT_ERROR)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Architecture shape\n",
        "\n",
        "The core design pattern is event-driven and deliberately places the copilot after evidence assembly. This reduces hallucination risk and ensures the model reasons over a structured context package instead of improvising from partial or ad hoc data."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "architecture = {\n",
        "    \"nodes\": [\n",
        "        \"Azure Monitor Alerts / Incidents\",\n",
        "        \"Event Grid\",\n",
        "        \"Azure Function: Context Builder\",\n",
        "        \"Azure Monitor Logs\",\n",
        "        \"Azure Resource Graph\",\n",
        "        \"CMDB / Approved Metadata\",\n",
        "        \"Grounded Evidence Package\",\n",
        "        \"Ops Copilot\",\n",
        "        \"Action Needed?\",\n",
        "        \"Operator Guidance\",\n",
        "        \"Approval Gate\",\n",
        "        \"Automation Runbook\",\n",
        "        \"Azure Resources\",\n",
        "        \"Audit Log / Change Record\",\n",
        "    ],\n",
        "    \"edges\": [\n",
        "        (\"Azure Monitor Alerts / Incidents\", \"Event Grid\"),\n",
        "        (\"Event Grid\", \"Azure Function: Context Builder\"),\n",
        "        (\"Azure Function: Context Builder\", \"Azure Monitor Logs\"),\n",
        "        (\"Azure Function: Context Builder\", \"Azure Resource Graph\"),\n",
        "        (\"Azure Function: Context Builder\", \"CMDB / Approved Metadata\"),\n",
        "        (\"Azure Monitor Logs\", \"Grounded Evidence Package\"),\n",
        "        (\"Azure Resource Graph\", \"Grounded Evidence Package\"),\n",
        "        (\"CMDB / Approved Metadata\", \"Grounded Evidence Package\"),\n",
        "        (\"Grounded Evidence Package\", \"Ops Copilot\"),\n",
        "        (\"Ops Copilot\", \"Action Needed?\"),\n",
        "        (\"Action Needed?\", \"Operator Guidance\"),\n",
        "        (\"Action Needed?\", \"Approval Gate\"),\n",
        "        (\"Approval Gate\", \"Automation Runbook\"),\n",
        "        (\"Automation Runbook\", \"Azure Resources\"),\n",
        "        (\"Automation Runbook\", \"Audit Log / Change Record\"),\n",
        "    ],\n",
        "}\n",
        "\n",
        "print(json.dumps(architecture, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Required environment variables for live Azure validation\n",
        "\n",
        "If you want to run the live Azure evidence builder instead of the offline mock path, set these variables in the notebook environment:\n",
        "\n",
        "- `AZURE_SUBSCRIPTION_ID`\n",
        "- `AZURE_LOG_ANALYTICS_WORKSPACE_ID`\n",
        "- `AZURE_RESOURCE_ID`\n",
        "\n",
        "Authentication is expected to come from `DefaultAzureCredential`, such as Azure CLI login, managed identity, or developer credentials."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Grounded evidence package builder\n",
        "\n",
        "This example assembles telemetry, inventory, and approved metadata into one structured object. The notebook includes a safe offline fallback so you can validate the pattern even without Azure access."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def build_evidence_package(subscription_id=None, workspace_id=None, resource_id=None, use_live_azure=False):\n",
        "    approved_context = {\n",
        "        \"serviceTier\": \"mission-critical\",\n",
        "        \"owner\": \"ops@contoso.com\",\n",
        "        \"environment\": \"prod\",\n",
        "        \"approvedRunbooks\": [\"Restart-AppService\", \"Scale-Out-AppService\"],\n",
        "        \"escalationPolicy\": \"sev1-page-primary-and-secondary\",\n",
        "    }\n",
        "\n",
        "    if use_live_azure and AZURE_SDK_AVAILABLE and subscription_id and workspace_id and resource_id:\n",
        "        cred = DefaultAzureCredential()\n",
        "        logs = LogsQueryClient(cred)\n",
        "        arg = ResourceGraphClient(cred)\n",
        "\n",
        "        kql = f\"AzureActivity | where ResourceId =~ '{resource_id}' | top 5 by TimeGenerated desc\"\n",
        "        log_result = logs.query_workspace(workspace_id, kql, timespan=None)\n",
        "        log_rows = []\n",
        "        if getattr(log_result, \"tables\", None):\n",
        "            for row in log_result.tables[0].rows:\n",
        "                log_rows.append([str(x) for x in row])\n",
        "\n",
        "        arg_req = QueryRequest(\n",
        "            subscriptions=[subscription_id],\n",
        "            query=f\"Resources | where id =~ '{resource_id}'\"\n",
        "        )\n",
        "        resource_rows = list(arg.resources(arg_req).data)\n",
        "\n",
        "        evidence = {\n",
        "            \"resourceId\": resource_id,\n",
        "            \"recentActivity\": log_rows,\n",
        "            \"resourceMetadata\": resource_rows,\n",
        "            \"approvedContext\": approved_context,\n",
        "            \"source\": \"live-azure\",\n",
        "            \"generatedAt\": datetime.now(timezone.utc).isoformat(),\n",
        "        }\n",
        "        return evidence\n",
        "\n",
        "    now = datetime.now(timezone.utc)\n",
        "    resource_id = resource_id or \"/subscriptions/000.../resourceGroups/prod-rg/providers/Microsoft.Compute/virtualMachines/app-01\"\n",
        "    evidence = {\n",
        "        \"resourceId\": resource_id,\n",
        "        \"recentActivity\": [\n",
        "            {\n",
        "                \"TimeGenerated\": (now - timedelta(minutes=3)).isoformat(),\n",
        "                \"OperationName\": \"Microsoft.Compute/virtualMachines/restart/action\",\n",
        "                \"Caller\": \"oncall@contoso.com\",\n",
        "                \"ActivityStatus\": \"Succeeded\",\n",
        "            },\n",
        "            {\n",
        "                \"TimeGenerated\": (now - timedelta(minutes=7)).isoformat(),\n",
        "                \"OperationName\": \"Microsoft.Insights/metricAlerts/Activated/Action\",\n",
        "                \"Caller\": \"AzureMonitor\",\n",
        "                \"ActivityStatus\": \"Succeeded\",\n",
        "            },\n",
        "        ],\n",
        "        \"resourceMetadata\": [\n",
        "            {\n",
        "                \"id\": resource_id,\n",
        "                \"name\": \"app-01\",\n",
        "                \"type\": \"microsoft.compute/virtualmachines\",\n",
        "                \"location\": \"eastus\",\n",
        "                \"tags\": {\n",
        "                    \"service\": \"payments\",\n",
        "                    \"tier\": \"app\",\n",
        "                    \"owner\": \"ops@contoso.com\",\n",
        "                    \"environment\": \"prod\",\n",
        "                },\n",
        "            }\n",
        "        ],\n",
        "        \"approvedContext\": approved_context,\n",
        "        \"source\": \"mock\",\n",
        "        \"generatedAt\": now.isoformat(),\n",
        "    }\n",
        "    return evidence\n",
        "\n",
        "subscription_id = os.getenv(\"AZURE_SUBSCRIPTION_ID\")\n",
        "workspace_id = os.getenv(\"AZURE_LOG_ANALYTICS_WORKSPACE_ID\")\n",
        "resource_id = os.getenv(\"AZURE_RESOURCE_ID\")\n",
        "\n",
        "use_live = all([subscription_id, workspace_id, resource_id])\n",
        "evidence = build_evidence_package(subscription_id, workspace_id, resource_id, use_live_azure=use_live)\n",
        "print(json.dumps(evidence, indent=2, default=str))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Severity classification and remediation eligibility\n",
        "\n",
        "This policy function separates investigation from remediation. In production, the default behavior is diagnosis and summary only unless approval is explicitly present."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def classify_incident(cpu_pct: float, error_rate: float, env: str, has_approval: bool) -> dict:\n",
        "    severity = \"sev3\"\n",
        "    if cpu_pct > 90 or error_rate > 0.05:\n",
        "        severity = \"sev2\"\n",
        "    if cpu_pct > 95 and error_rate > 0.10:\n",
        "        severity = \"sev1\"\n",
        "\n",
        "    can_remediate = env != \"prod\" or has_approval\n",
        "    allowed_actions = [\"diagnose\", \"summarize\"]\n",
        "    if can_remediate:\n",
        "        allowed_actions.append(\"restart-service\")\n",
        "\n",
        "    return {\n",
        "        \"severity\": severity,\n",
        "        \"environment\": env,\n",
        "        \"approvalPresent\": has_approval,\n",
        "        \"allowedActions\": allowed_actions,\n",
        "    }\n",
        "\n",
        "scenarios = [\n",
        "    {\"cpu_pct\": 72, \"error_rate\": 0.01, \"env\": \"nonprod\", \"has_approval\": False},\n",
        "    {\"cpu_pct\": 93, \"error_rate\": 0.06, \"env\": \"prod\", \"has_approval\": False},\n",
        "    {\"cpu_pct\": 97, \"error_rate\": 0.12, \"env\": \"prod\", \"has_approval\": False},\n",
        "    {\"cpu_pct\": 97, \"error_rate\": 0.12, \"env\": \"prod\", \"has_approval\": True},\n",
        "]\n",
        "\n",
        "results = [classify_incident(**s) for s in scenarios]\n",
        "pd.DataFrame(results)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Deployment workflow artifact\n",
        "\n",
        "The original post included a GitHub Actions workflow for repeatable deployment. Here it is represented as a Python string and parsed as YAML so you can validate structure inside the notebook."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "github_actions_yaml = dedent('''\n",
        "name: deploy-ops-copilot\n",
        "on:\n",
        "  workflow_dispatch:\n",
        "jobs:\n",
        "  deploy:\n",
        "    runs-on: ubuntu-latest\n",
        "    steps:\n",
        "      - uses: actions/checkout@v4\n",
        "      - uses: azure/login@v2\n",
        "        with:\n",
        "          client-id: ${{ secrets.AZURE_CLIENT_ID }}\n",
        "          tenant-id: ${{ secrets.AZURE_TENANT_ID }}\n",
        "          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}\n",
        "      - uses: azure/arm-deploy@v2\n",
        "        with:\n",
        "          resourceGroupName: rg-ops-copilot-prod\n",
        "          template: infra/main.bicep\n",
        "          parameters: environment=prod functionAppName=func-opscopilot-prod\n",
        "''').strip()\n",
        "\n",
        "print(github_actions_yaml)\n",
        "print(\"\\nParsed keys:\", list(yaml.safe_load(github_actions_yaml).keys()))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Managed-identity Azure Function infrastructure\n",
        "\n",
        "This Bicep template provisions a Function App with a system-assigned managed identity and HTTPS-only access. In this notebook, the artifact is stored and lightly inspected rather than deployed."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "function_bicep = dedent('''\n",
        "param location string = resourceGroup().location\n",
        "param functionAppName string\n",
        "param environment string\n",
        "\n",
        "resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {\n",
        "  name: 'asp-${functionAppName}'\n",
        "  location: location\n",
        "  sku: { name: 'Y1', tier: 'Dynamic' }\n",
        "}\n",
        "\n",
        "resource app 'Microsoft.Web/sites@2023-12-01' = {\n",
        "  name: functionAppName\n",
        "  location: location\n",
        "  kind: 'functionapp'\n",
        "  identity: { type: 'SystemAssigned' }\n",
        "  properties: {\n",
        "    serverFarmId: plan.id\n",
        "    httpsOnly: true\n",
        "    siteConfig: { appSettings: [{ name: 'ENVIRONMENT'; value: environment }] }\n",
        "  }\n",
        "}\n",
        "''').strip()\n",
        "\n",
        "print(function_bicep)\n",
        "print(\"\\nContains managed identity:\", \"SystemAssigned\" in function_bicep)\n",
        "print(\"Contains HTTPS only:\", \"httpsOnly: true\" in function_bicep)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Least-privilege role assignments\n",
        "\n",
        "This Bicep example grants read-only access for telemetry and inventory queries. The notebook validates that the template references Reader and Monitoring Reader role definition IDs."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "roles_bicep = dedent('''\n",
        "param principalId string\n",
        "param subscriptionId string = subscription().subscriptionId\n",
        "\n",
        "resource monitorReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = {\n",
        "  name: guid(subscriptionId, principalId, 'monitor-reader')\n",
        "  scope: subscription()\n",
        "  properties: {\n",
        "    principalId: principalId\n",
        "    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '43d0d8ad-25c7-4714-9337-8ba259a9fe05')\n",
        "    principalType: 'ServicePrincipal'\n",
        "  }\n",
        "}\n",
        "\n",
        "resource reader 'Microsoft.Authorization/roleAssignments@2022-04-01' = {\n",
        "  name: guid(subscriptionId, principalId, 'reader')\n",
        "  scope: subscription()\n",
        "  properties: {\n",
        "    principalId: principalId\n",
        "    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')\n",
        "    principalType: 'ServicePrincipal'\n",
        "  }\n",
        "}\n",
        "''').strip()\n",
        "\n",
        "print(roles_bicep)\n",
        "print(\"\\nValidation:\")\n",
        "print({\n",
        "    \"has_monitoring_reader_role\": \"43d0d8ad-25c7-4714-9337-8ba259a9fe05\" in roles_bicep,\n",
        "    \"has_reader_role\": \"acdd72a7-3385-48ef-bd42-f606fba81ae7\" in roles_bicep,\n",
        "    \"scoped_at_subscription\": \"scope: subscription()\" in roles_bicep,\n",
        "})"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Production remediation policy artifact\n",
        "\n",
        "The blog post emphasized a machine-readable policy that distinguishes production from non-production. This example parses the YAML policy and tests whether actions are allowed under each environment."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "policy_yaml = dedent('''\n",
        "environments:\n",
        "  prod:\n",
        "    approvalRequired: true\n",
        "    allowedActions:\n",
        "      - restart-service\n",
        "      - scale-out\n",
        "  nonprod:\n",
        "    approvalRequired: false\n",
        "    allowedActions:\n",
        "      - restart-service\n",
        "      - scale-out\n",
        "      - recycle-worker\n",
        "''').strip()\n",
        "\n",
        "policy = yaml.safe_load(policy_yaml)\n",
        "print(policy)\n",
        "\n",
        "def is_action_allowed(policy: dict, env: str, action: str, has_approval: bool) -> dict:\n",
        "    env_policy = policy[\"environments\"][env]\n",
        "    approval_required = env_policy.get(\"approvalRequired\", False)\n",
        "    allowed_actions = env_policy.get(\"allowedActions\", [])\n",
        "    allowed = action in allowed_actions and (not approval_required or has_approval)\n",
        "    return {\n",
        "        \"environment\": env,\n",
        "        \"action\": action,\n",
        "        \"approvalRequired\": approval_required,\n",
        "        \"hasApproval\": has_approval,\n",
        "        \"allowed\": allowed,\n",
        "    }\n",
        "\n",
        "tests = [\n",
        "    is_action_allowed(policy, \"prod\", \"restart-service\", False),\n",
        "    is_action_allowed(policy, \"prod\", \"restart-service\", True),\n",
        "    is_action_allowed(policy, \"nonprod\", \"recycle-worker\", False),\n",
        "    is_action_allowed(policy, \"prod\", \"recycle-worker\", True),\n",
        "]\n",
        "\n",
        "pd.DataFrame(tests)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Guarded remediation runbook logic\n",
        "\n",
        "The original example used PowerShell to block production remediation without an approval ticket. Here, the same control is modeled in Python so the behavior can be validated directly in the notebook."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def invoke_guarded_remediation(automation_account=\"aa-ops-prod\", resource_group=\"rg-ops-prod\", runbook_name=\"Restart-AppService\", target_resource=\"/subscriptions/000.../resourceGroups/prod-rg/providers/Microsoft.Web/sites/app-prod\", approval_ticket=\"\"):\n",
        "    if not approval_ticket or not str(approval_ticket).strip():\n",
        "        raise ValueError(\"Approval ticket is required for production remediation.\")\n",
        "\n",
        "    params = {\n",
        "        \"TargetResource\": target_resource,\n",
        "        \"ApprovalTicket\": approval_ticket,\n",
        "        \"RequestedBy\": \"OpsCopilot\",\n",
        "    }\n",
        "\n",
        "    return {\n",
        "        \"automationAccount\": automation_account,\n",
        "        \"resourceGroup\": resource_group,\n",
        "        \"runbookName\": runbook_name,\n",
        "        \"parameters\": params,\n",
        "        \"status\": \"queued\",\n",
        "    }\n",
        "\n",
        "try:\n",
        "    invoke_guarded_remediation(approval_ticket=\"\")\n",
        "except Exception as e:\n",
        "    print(\"Blocked as expected:\", e)\n",
        "\n",
        "print(invoke_guarded_remediation(approval_ticket=\"CHG-2026-000123\"))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## End-to-end incident flow simulation\n",
        "\n",
        "This final validation cell simulates the full operating model: incident arrives, evidence is assembled, policy is evaluated, approval is checked, and action is either blocked or queued. This demonstrates the intended order of operations: evidence, interpretation, recommendation, action request."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def simulate_incident_flow(cpu_pct, error_rate, env, has_approval):\n",
        "    evidence = build_evidence_package(use_live_azure=False)\n",
        "    classification = classify_incident(cpu_pct=cpu_pct, error_rate=error_rate, env=env, has_approval=has_approval)\n",
        "\n",
        "    recommendation = {\n",
        "        \"incidentSummary\": f\"CPU at {cpu_pct}%, error rate at {error_rate:.2%}\",\n",
        "        \"recommendedRunbook\": \"Restart-AppService\" if \"restart-service\" in classification[\"allowedActions\"] else None,\n",
        "        \"reasoningOrder\": [\"evidence\", \"interpretation\", \"recommendation\", \"action-request\"],\n",
        "    }\n",
        "\n",
        "    action_result = None\n",
        "    if recommendation[\"recommendedRunbook\"]:\n",
        "        ticket = \"CHG-2026-000123\" if has_approval else \"\"\n",
        "        try:\n",
        "            action_result = invoke_guarded_remediation(approval_ticket=ticket)\n",
        "        except Exception as e:\n",
        "            action_result = {\"status\": \"blocked\", \"reason\": str(e)}\n",
        "    else:\n",
        "        action_result = {\"status\": \"not-eligible\", \"reason\": \"No approved remediation action available\"}\n",
        "\n",
        "    return {\n",
        "        \"evidenceSource\": evidence[\"source\"],\n",
        "        \"classification\": classification,\n",
        "        \"recommendation\": recommendation,\n",
        "        \"actionResult\": action_result,\n",
        "    }\n",
        "\n",
        "flows = [\n",
        "    simulate_incident_flow(97, 0.12, \"prod\", False),\n",
        "    simulate_incident_flow(97, 0.12, \"prod\", True),\n",
        "    simulate_incident_flow(92, 0.06, \"nonprod\", False),\n",
        "]\n",
        "\n",
        "print(json.dumps(flows, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Summary\n",
        "\n",
        "This notebook validated the core design pattern for a proactive Azure ops copilot: assemble grounded evidence first, let the copilot interpret within boundaries, and require explicit approval before any production remediation. It also demonstrated how to separate read-only investigation from write-capable automation, enforce least privilege, and encode policy in machine-readable artifacts.\n",
        "\n",
        "## Next Steps\n",
        "\n",
        "- Replace mock evidence with live Azure Monitor and Resource Graph queries.\n",
        "- Scope managed identity permissions below subscription where possible.\n",
        "- Implement a real approval workflow in Copilot Studio or Power Automate.\n",
        "- Add audit persistence for every recommendation, approval, and action.\n",
        "- Start with one service, one incident class, and one responder cohort before expanding."
      ]
    }
  ]
}