{
  "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": "Azure Functions Skills Is More Important Than It Looks: It Turns Serverless Into an Agent Execution Surface",
      "slug": "azure-functions-skills-is-more-important-than-it-looks-it-tu",
      "generated_by": "LinkedIn Post Generator + Azure OpenAI",
      "generated_at": "2026-08-06T16:38:51.930Z"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# Azure Functions Skills Is More Important Than It Looks: It Turns Serverless Into an Agent Execution Surface\n",
        "\n",
        "This notebook turns the blog post into a hands-on validation workflow. It demonstrates how a narrowly scoped Azure Function skill can act as a governed action boundary for agents, with strong input validation, bounded responses, and simple architecture checks.\n",
        "\n",
        "The goal is not to let an agent call every backend directly, but to show how one explicit, reversible, and observable action can be exposed safely."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "%pip install -q pandas"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "import json\n",
        "import re\n",
        "from dataclasses import dataclass\n",
        "from typing import Any, Dict, Optional\n",
        "\n",
        "import pandas as pd"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Why this pattern matters\n",
        "\n",
        "The blog argues that Azure Functions Skills are important because they create a narrow execution surface between agent reasoning and enterprise side effects. Instead of exposing raw backend tools, you expose a governed skill with progressive disclosure, validation, monitoring, and constrained outputs.\n",
        "\n",
        "A practical starting point is one reversible business action with a painfully narrow contract."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Example 1: Azure Function skill with narrow action, input validation, and bounded response contract\n",
        "\n",
        "This example recreates the blog's Azure Function pattern in Python so it can be tested locally in a notebook. Because the notebook may not have the Azure Functions runtime installed, the code includes lightweight stand-ins for `HttpRequest` and `HttpResponse` while preserving the same behavior."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "import json\n",
        "import re\n",
        "from dataclasses import dataclass\n",
        "from typing import Any, Dict\n",
        "\n",
        "@dataclass\n",
        "class HttpRequest:\n",
        "    body: Dict[str, Any]\n",
        "\n",
        "    def get_json(self) -> Dict[str, Any]:\n",
        "        return self.body\n",
        "\n",
        "@dataclass\n",
        "class HttpResponse:\n",
        "    body: str\n",
        "    status_code: int = 200\n",
        "    mimetype: str = \"application/json\"\n",
        "\n",
        "    def json(self) -> Dict[str, Any]:\n",
        "        return json.loads(self.body)\n",
        "\n",
        "\n",
        "def main(req: HttpRequest) -> HttpResponse:\n",
        "    body = req.get_json()\n",
        "    customer_id = str(body.get(\"customerId\", \"\")).strip()\n",
        "    if not re.fullmatch(r\"CUST-\\d{6}\", customer_id):\n",
        "        return HttpResponse(\n",
        "            json.dumps({\"ok\": False, \"code\": \"INVALID_CUSTOMER_ID\"}),\n",
        "            status_code=400,\n",
        "            mimetype=\"application/json\",\n",
        "        )\n",
        "\n",
        "    result = {\"customerId\": customer_id, \"tier\": \"gold\", \"eligible\": True}\n",
        "    response = {\"ok\": True, \"action\": \"GetLoyaltyEligibility\", \"result\": result}\n",
        "    return HttpResponse(json.dumps(response), mimetype=\"application/json\")\n",
        "\n",
        "\n",
        "valid_req = HttpRequest({\"customerId\": \"CUST-123456\"})\n",
        "invalid_req = HttpRequest({\"customerId\": \"123456\"})\n",
        "\n",
        "valid_resp = main(valid_req)\n",
        "invalid_resp = main(invalid_req)\n",
        "\n",
        "print(\"Valid status:\", valid_resp.status_code)\n",
        "print(json.dumps(valid_resp.json(), indent=2))\n",
        "print()\n",
        "print(\"Invalid status:\", invalid_resp.status_code)\n",
        "print(json.dumps(invalid_resp.json(), indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Validate the action boundary with test cases\n",
        "\n",
        "A narrow skill contract is only useful if it fails safely. The following checks exercise valid and invalid inputs to confirm that the function accepts only the expected customer ID format and returns a bounded response."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "test_inputs = [\n",
        "    {\"customerId\": \"CUST-000001\"},\n",
        "    {\"customerId\": \"CUST-999999\"},\n",
        "    {\"customerId\": \"cust-123456\"},\n",
        "    {\"customerId\": \"CUST-12345\"},\n",
        "    {\"customerId\": \"CUST-1234567\"},\n",
        "    {\"customerId\": \" CUST-123456 \"},\n",
        "    {\"customerId\": \"DROP TABLE\"},\n",
        "    {},\n",
        "]\n",
        "\n",
        "rows = []\n",
        "for payload in test_inputs:\n",
        "    resp = main(HttpRequest(payload))\n",
        "    rows.append({\n",
        "        \"input\": payload,\n",
        "        \"status_code\": resp.status_code,\n",
        "        \"response\": resp.json(),\n",
        "    })\n",
        "\n",
        "pd.DataFrame(rows)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Example 2: Minimal response shaping to keep the skill contract bounded and agent-safe\n",
        "\n",
        "This helper enforces a small, typed response contract. It prevents unsupported values from leaking through and keeps the agent-facing output focused on the governed business question rather than backend internals."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def shape_skill_response(customer_id: str, tier: str, eligible: bool) -> dict:\n",
        "    allowed_tiers = {\"bronze\", \"silver\", \"gold\"}\n",
        "    if tier not in allowed_tiers:\n",
        "        raise ValueError(\"Unsupported tier\")\n",
        "    return {\n",
        "        \"ok\": True,\n",
        "        \"action\": \"GetLoyaltyEligibility\",\n",
        "        \"result\": {\n",
        "            \"customerId\": customer_id,\n",
        "            \"tier\": tier,\n",
        "            \"eligible\": bool(eligible),\n",
        "        },\n",
        "    }\n",
        "\n",
        "\n",
        "print(json.dumps(shape_skill_response(\"CUST-123456\", \"gold\", True), indent=2))\n",
        "\n",
        "try:\n",
        "    shape_skill_response(\"CUST-123456\", \"platinum\", True)\n",
        "except Exception as e:\n",
        "    print(\"\\nError:\", e)"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Compare bounded vs. overexposed responses\n",
        "\n",
        "The blog's core lesson is to avoid spraying backend detail back to the agent. This cell contrasts a safe response shape with an unsafe one that exposes unnecessary internal fields."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "bounded = shape_skill_response(\"CUST-123456\", \"gold\", True)\n",
        "\n",
        "overexposed = {\n",
        "    \"ok\": True,\n",
        "    \"action\": \"GetLoyaltyEligibility\",\n",
        "    \"result\": {\n",
        "        \"customerId\": \"CUST-123456\",\n",
        "        \"tier\": \"gold\",\n",
        "        \"eligible\": True,\n",
        "        \"crmRecordId\": \"7f1c2a9e-raw-internal-id\",\n",
        "        \"queueName\": \"priority-retention-west\",\n",
        "        \"lastAgent\": \"agent_4821\",\n",
        "        \"internalNotes\": \"Customer flagged for manual review in legacy CRM\",\n",
        "        \"backendSystem\": \"crm-prod-02\",\n",
        "    },\n",
        "}\n",
        "\n",
        "comparison = pd.DataFrame([\n",
        "    {\"contract\": \"bounded\", \"keys\": list(bounded[\"result\"].keys()), \"field_count\": len(bounded[\"result\"])} ,\n",
        "    {\"contract\": \"overexposed\", \"keys\": list(overexposed[\"result\"].keys()), \"field_count\": len(overexposed[\"result\"])}\n",
        "])\n",
        "comparison"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Example 3: Inventory Function App settings and review identity, hosting, and monitoring\n",
        "\n",
        "The original blog included a PowerShell example using Azure CLI. Here, that logic is translated into Python so it can run in a notebook and still support hands-on validation.\n",
        "\n",
        "This version uses mocked Azure metadata by default, but the same shaping logic can be pointed at real CLI or SDK outputs in your environment."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def inventory_function_app(app: dict, config: list) -> dict:\n",
        "    def get_setting(name: str) -> Optional[str]:\n",
        "        for item in config:\n",
        "            if item.get(\"name\") == name:\n",
        "                return item.get(\"value\")\n",
        "        return None\n",
        "\n",
        "    return {\n",
        "        \"Name\": app.get(\"name\"),\n",
        "        \"Kind\": app.get(\"kind\"),\n",
        "        \"HostPlan\": app.get(\"serverFarmId\"),\n",
        "        \"HttpsOnly\": app.get(\"httpsOnly\"),\n",
        "        \"ManagedIdentity\": (app.get(\"identity\") or {}).get(\"type\"),\n",
        "        \"AppInsightsKeySet\": get_setting(\"APPLICATIONINSIGHTS_CONNECTION_STRING\") is not None,\n",
        "        \"WorkerRuntime\": get_setting(\"FUNCTIONS_WORKER_RUNTIME\"),\n",
        "    }\n",
        "\n",
        "\n",
        "mock_app = {\n",
        "    \"name\": \"fn-agent-skill-prod\",\n",
        "    \"kind\": \"functionapp,linux\",\n",
        "    \"serverFarmId\": \"/subscriptions/000/resourceGroups/rg-demo/providers/Microsoft.Web/serverfarms/plan-consumption\",\n",
        "    \"httpsOnly\": True,\n",
        "    \"identity\": {\"type\": \"SystemAssigned\"},\n",
        "}\n",
        "\n",
        "mock_config = [\n",
        "    {\"name\": \"APPLICATIONINSIGHTS_CONNECTION_STRING\", \"value\": \"InstrumentationKey=example;IngestionEndpoint=https://example\"},\n",
        "    {\"name\": \"FUNCTIONS_WORKER_RUNTIME\", \"value\": \"python\"},\n",
        "]\n",
        "\n",
        "inventory = inventory_function_app(mock_app, mock_config)\n",
        "print(json.dumps(inventory, indent=2))\n",
        "\n",
        "print(\"\\nReview prompts:\")\n",
        "print(\"- Identity enabled and least-privilege RBAC assigned?\")\n",
        "print(\"- Hosting plan matches latency/concurrency expectations?\")\n",
        "print(\"- Monitoring connected with Application Insights and alerts?\")"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Required variables for real Azure validation\n",
        "\n",
        "If you adapt the inventory example to query a live Azure environment, you will typically need:\n",
        "\n",
        "- `AZURE_SUBSCRIPTION_ID`\n",
        "- `AZURE_RESOURCE_GROUP`\n",
        "- `AZURE_FUNCTION_APP`\n",
        "- Azure CLI login context or managed identity credentials\n",
        "\n",
        "This notebook does not require secrets as written because it uses mocked data for safe local validation."
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Architecture pattern from the post\n",
        "\n",
        "The practical pattern described in the blog is:\n",
        "\n",
        "- Agent + skill catalog at the interaction layer\n",
        "- Azure Functions at the action-execution layer\n",
        "- APIs for synchronous operations\n",
        "- Events for async work\n",
        "- Model service alongside the stack, not fused into it\n",
        "\n",
        "The next cell renders that flow as a simple executable text diagram."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "architecture_steps = [\n",
        "    \"Agent receives user goal\",\n",
        "    \"Selects Azure Function skill\",\n",
        "    \"Function validates narrow inputs\",\n",
        "    \"Executes bounded business action\",\n",
        "    \"Returns small typed contract\",\n",
        "    \"Agent decides next step\",\n",
        "]\n",
        "\n",
        "for i, step in enumerate(architecture_steps, start=1):\n",
        "    print(f\"{i}. {step}\")\n",
        "\n",
        "print(\"\\nInvalid input path:\")\n",
        "print(\"- Function returns safe error for agent handling\")"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Operational checklist\n",
        "\n",
        "The enterprise value here is control, not novelty. Before rollout, platform teams should define how each skill is productized, constrained, observed, and approved."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "checklist = pd.DataFrame({\n",
        "    \"recommendation\": [\n",
        "        \"Productize each skill-facing operation\",\n",
        "        \"Keep scope narrow and purpose explicit\",\n",
        "        \"Define failure behavior before rollout\",\n",
        "        \"Refuse broad destructive actions until approval paths are proven\",\n",
        "        \"Verify traces from agent decision to function call to downstream system\",\n",
        "    ],\n",
        "    \"status\": [\"todo\"] * 5,\n",
        "})\n",
        "\n",
        "checklist"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Failure mode simulation\n",
        "\n",
        "The blog highlights that action design, not just model quality, determines safety. This simple simulation shows how a broad action surface can amplify the impact of a bad parameter, while a narrow skill sharply limits blast radius."
      ]
    },
    {
      "cell_type": "code",
      "metadata": {},
      "source": [
        "def broad_action(case_queue: str, record_count: int) -> dict:\n",
        "    return {\n",
        "        \"ok\": True,\n",
        "        \"action\": \"OpenCaseQueue\",\n",
        "        \"queue\": case_queue,\n",
        "        \"records_affected\": record_count,\n",
        "    }\n",
        "\n",
        "\n",
        "def narrow_action(customer_id: str) -> dict:\n",
        "    resp = main(HttpRequest({\"customerId\": customer_id}))\n",
        "    return {\"status_code\": resp.status_code, \"body\": resp.json()}\n",
        "\n",
        "\n",
        "broad_result = broad_action(\"wrong-customer-queue\", 11000)\n",
        "narrow_result_valid = narrow_action(\"CUST-123456\")\n",
        "narrow_result_invalid = narrow_action(\"wrong-customer-queue\")\n",
        "\n",
        "print(\"Broad action result:\")\n",
        "print(json.dumps(broad_result, indent=2))\n",
        "print(\"\\nNarrow action valid result:\")\n",
        "print(json.dumps(narrow_result_valid, indent=2))\n",
        "print(\"\\nNarrow action invalid result:\")\n",
        "print(json.dumps(narrow_result_invalid, indent=2))"
      ],
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Summary\n",
        "\n",
        "This notebook validated the central claim of the post: Azure Functions Skills can serve as an enterprise action plane for agents when the contract is narrow, validated, and observable. The key design pattern is to expose one governed question or action at a time rather than handing the model a bag of raw backend tools.\n",
        "\n",
        "## Next Steps\n",
        "\n",
        "- Replace mocked request and app metadata with real Azure Functions and Azure CLI or SDK calls\n",
        "- Add authentication and authorization checks to the skill boundary\n",
        "- Emit structured logs and traces for every agent-to-function invocation\n",
        "- Define approval paths for any destructive or high-impact action\n",
        "- Expand from one reversible skill to a small catalog with progressive disclosure"
      ]
    }
  ]
}