Author: Deepak Waghmare

  • The Readiness Probe That Turned a Deploy Into an Outage

    A routine deploy. The new pods come up, and for ninety seconds the entire service returns 503 — even though the previous version was perfectly healthy the whole time. The culprit wasn’t the new code. It was a readiness probe pointed at the wrong thing.

    Readiness and liveness are opposites, not synonyms

    A readiness probe decides whether a pod receives traffic. A liveness probe decides whether a pod gets killed and restarted. Teams wire them identically and treat them as interchangeable, but they want opposite temperaments: liveness should be lazy and forgiving; readiness should be quick and honest. Confuse them and a slow-starting app either gets killed mid-boot because liveness is too aggressive, or gets declared “ready” before it can actually serve.

    The rollout death spiral

    Here’s how a bad readiness probe takes down a healthy service. The new ReplicaSet’s pods fail readiness. The rolling update, following maxUnavailable, has already begun retiring old pods — but the new ones never become ready to replace them. Endpoints drain faster than they fill. For a window, almost nothing is behind the Service, and clients get 503s from the exact mechanism that’s supposed to prevent this.

    # The trap: readiness hits a downstream dependency, with no warm-up
    readinessProbe:
      httpGet:
        path: /healthz        # this handler also pings the database
        port: 8080
      initialDelaySeconds: 0  # checked before the app can even listen
      periodSeconds: 5
      failureThreshold: 1     # one blip = pulled from rotation

    Two mistakes compound. The health endpoint checks a downstream dependency, so a brief database hiccup marks every replica unready at once — a self-inflicted full outage. And failureThreshold: 1 with no startup grace means the smallest transient becomes an eviction from the load balancer.

    The fix: a shallow check plus a startupProbe

    # Give slow starters room without loosening liveness
    startupProbe:
      httpGet: { path: /healthz, port: 8080 }
      failureThreshold: 30
      periodSeconds: 5          # up to 150s to boot, then normal probes take over
    readinessProbe:
      httpGet:
        path: /ready            # answers ONLY "can this process serve?" — no deep deps
        port: 8080
      periodSeconds: 5
      failureThreshold: 3

    Split the endpoints. /ready answers “is this process able to serve a request,” nothing more — don’t fold the database into it, or one slow dependency yanks every pod out of rotation simultaneously. Use a startupProbe to grant slow-booting apps their warm-up time instead of weakening liveness. And set maxUnavailable: 0 on critical rollouts so Kubernetes won’t retire an old pod until a new one is genuinely ready.

    The lesson

    Readiness is a promise to your load balancer, not a general health dashboard. Make it answer the narrowest possible question — “send me traffic now?” — and nothing else. Every dependency you fold into that check is another way for one small failure to become a total one.

    Probe configs and a reproducible rollout demo are on GitHub: github.com/waghmaredb/vexpose-labs. Got burned by a probe in a way I didn’t cover? Tell me on LinkedIn or X.

  • Measuring Your AI Exit Cost: An Architecture Review

    On LinkedIn, I make the executive argument: your AI problem is not cost, it is dependency. Cost is what you pay this quarter; dependency is what you pay at every renewal, for a decade. This post is the engineering companion. If dependency is the real exposure, it should be measurable — and anything measurable can be architected down.

    So: how do you actually measure your AI exit cost? Not as a slide. As an audit.

    Exit cost is a stack, not a number

    Teams tend to equate switching providers with changing an API key. That was roughly true in 2023. In an agentic estate, provider coupling has accumulated in five distinct layers, and each has its own unwind cost.

    Layer 1 — API surface

    The cheapest layer to fix and the first to audit. If application code calls a provider SDK directly, every model reference is a migration line-item. The fix is a gateway: one internal endpoint, provider adapters behind it, model names resolved by policy rather than hard-coded. If you cannot change your default model in one config file, start here — nothing else in this audit is testable until you can.

    Layer 2 — Prompt and behavior coupling

    Prompts are tuned, consciously or not, to one model’s quirks: its instruction-following style, its formatting habits, its tool-calling dialect. This coupling is invisible until you swap models and watch pass rates drop. The countermeasure is an evaluation suite that defines correct behavior independently of any provider — golden datasets, structured-output checks, task-level assertions. Your eval suite is your portability contract. No evals, no measurable exit cost; only guesses.

    Layer 3 — Data gravity

    The heaviest layer. Embeddings are tied to the model that produced them: switch embedding models and every vector in the store must be regenerated — a compute bill and a re-indexing window that scales with your corpus. Fine-tunes are worse: weights trained on a proprietary base do not travel at all. Rule of thumb: prefer retrieval over fine-tuning wherever quality allows, and record the full re-embedding cost of your corpus as a standing line in the exit ledger. If you do not know that number today, that is finding #1 of your audit.

    Layer 4 — Orchestration and agent coupling

    Agent frameworks encode provider assumptions: tool-schema formats, context-window sizes, retry semantics, reasoning-token behavior. A workflow that plans, retrieves, calls tools, and self-corrects has ten to twenty coupling points where a chat app had one. Keep tool definitions in a neutral schema and translate at the gateway; treat any framework feature that only works on one provider as a loan you will repay at migration time.

    Layer 5 — Economic architecture

    Routing is where dependency becomes money. The arithmetic is one-sided even at public list prices: frontier models cost on the order of ten times more per token than the small and open-weight models that handle classification, extraction, and summarization perfectly well. Send every request to a frontier model and you pay the premium rate for work a cheaper tier would do identically — so a tiered blend lands at a fraction of a frontier-everything bill, with the exact multiple set by your own traffic mix. Measure yours; don’t borrow someone else’s number. But the deeper value of a routing tier is optionality: once classification, extraction, and summarization run on interchangeable smaller models — including open-weight ones you could self-host — the share of your workload that is truly captive shrinks to the frontier-only remainder. Price your renewal leverage as: the percentage of token volume that could move within 30 days without eval regression.

    The failover drill

    An exit cost you have never tested is a fiction. The drill is simple and quarterly: repoint the gateway’s default policy at your designated fallback (a second provider or an open-weight deployment), run the full eval suite, and record three numbers — engineering hours spent, eval pass-rate delta, and the workload percentage that moved cleanly.

    # gateway routing policy — failover drill
    default_tier:
      primary:   frontier-a
      fallback:  open-weight-70b   # drill target
    routes:
      classify|extract|summarize:  small-tier
      agentic|complex-reasoning:   default_tier
    drill:
      cadence: quarterly
      pass_criteria: eval_delta <= 2%
      record: [eng_hours, eval_delta, pct_moved]

    Those three numbers, tracked over time, are your exit cost — not an estimate, a measurement. In my experience the first drill is always humbling: the gap between the architecture diagram and the actual coupling is where the real dependency lives.

    The point is not to leave

    None of this is a plan to abandon your provider. It is the opposite of a plan: it is an option. An option you hold changes a renewal negotiation even if you never exercise it. An option you lack is a price you will accept.

    Rent intelligence if you choose to. Measure the exit before it is load-bearing. And never rent the operating model.

  • The Terraform Gotcha That Destroys the Resource You Meant to Keep

    You add one server to the middle of a list, run terraform plan, and Terraform announces it will destroy and recreate three resources you never touched. Nothing about them changed. This is the count trap, and it has caused more 2 a.m. incidents than almost any other Terraform footgun I know.

    Why a list index is a landmine

    When you build resources with count, Terraform addresses them by position: aws_instance.node[0], [1], [2]. That index is the resource’s identity in state. Remove or insert an element anywhere but the end and every index after it shifts — so [1] now points at what used to be [2]. Terraform doesn’t see “the list got shorter.” It sees “the thing at index 1 is a different thing now,” and the only way it knows to reconcile that is destroy-and-recreate.

    # The trap: identity is the position in the list
    variable "nodes" {
      default = ["web-a", "web-b", "web-c"]
    }
    
    resource "aws_instance" "node" {
      count = length(var.nodes)
      tags  = { Name = var.nodes[count.index] }
    }
    # Remove "web-a" and web-b and web-c each shift down one index —
    # Terraform destroys and recreates BOTH to "fix" the mismatch.

    The fix: address resources by a stable key, not a position

    for_each keys each resource by a string you control instead of an ordinal Terraform controls. Delete one entry and the others keep their identity, because their identity was never their position.

    # The fix: identity is a stable key
    variable "nodes" {
      default = ["web-a", "web-b", "web-c"]
    }
    
    resource "aws_instance" "node" {
      for_each = toset(var.nodes)
      tags     = { Name = each.key }
    }
    # The address is now aws_instance.node["web-b"].
    # Remove "web-a" and ONLY "web-a" is destroyed. The rest don't move.

    If you’re already on count

    Migrating isn’t rewrite-and-pray. Move the state entries to their new keyed addresses so Terraform keeps the existing resources instead of replacing them:

    terraform state mv 'aws_instance.node[1]' 'aws_instance.node["web-b"]'
    terraform state mv 'aws_instance.node[2]' 'aws_instance.node["web-c"]'
    # Then swap count for for_each in the config and plan — it should show no changes.

    Or, on Terraform 1.1+, declare the intent as code with a moved block and let the plan do the reshuffle for you. Either way, confirm the plan shows zero replacements before you apply. That plan output is the entire safety mechanism — read it.

    The lesson

    Use count only for genuinely anonymous, interchangeable replicas you’ll only ever scale from the end — three identical workers, nothing addressed individually. The moment a resource has a name, an identity, or a lifecycle of its own, key it with for_each. In infrastructure as code, how you address a thing is part of what the thing is. Get the identity wrong and Terraform will faithfully destroy production to make the map match the territory.

    More field notes and the full examples are on GitHub: github.com/waghmaredb/vexpose-labs. Hit a stranger Terraform footgun than this one? Trade war stories on LinkedIn or X.

  • A Reference Architecture for Enterprise RAG (That Won’t Hallucinate Your Docs)

    Every RAG demo works. You point a model at your docs, ask a question, get a fluent answer with a citation, and the room nods. Then it ships, someone asks about a policy that changed last quarter — and the system confidently cites the old version. The demo never showed you that failure mode, because the failure mode isn’t “the model can’t answer.” It’s “the model answers anyway.”

    That gap is where enterprise RAG loses trust — and it’s almost never the model’s fault. It’s retrieval handing over plausible-but-wrong context that a capable model then synthesizes beautifully. The tool is not the transformation: a bigger model makes a confident wrong answer more convincing, not less. The fix lives below the model, in retrieval and grounding. Here’s the architecture I’d stand up for RAG you can actually put in front of employees or customers.

    The failure mode: fluent, cited, and wrong

    Naive RAG is three lines: embed the query, pull the top-k nearest chunks, stuff them in the prompt. It fails three ways at once. Vector search alone misses the exact terms — part numbers, error codes, policy names — that keyword search would nail. Without reranking, “near in embedding space” isn’t “actually answers the question.” And with no relevance floor, the pipeline always returns something, so the model always has context to rationalize an answer from — even when the right answer is “we don’t have that.”

    The result is the worst kind of wrong: confident, well-written, and cited. Everything below exists to make the system able to say “I don’t have that in the sources.”

    The reference architecture

      Query
         |
         v
      Hybrid Retrieval      BM25 (keyword) + vector
         |
         v
      Rerank                cross-encoder scores the union
         |
         v
      Relevance floor  --- nothing relevant --->  Refuse ("not in the sources")
         |
      context clears floor
         |
         v
      Grounded generation   answer ONLY from context, cite every claim
         |
         v
      Answer + citations
         |
         v
      Eval + observability  groundedness + citation checks  --->  tune retrieval

    1. Ingestion & chunking. How you split documents is a retrieval decision, not a preprocessing chore. Chunk too big and the reranker drowns in noise; too small and you sever the context a claim needs. Preserve structure (headings, tables) and keep the source metadata — you’ll need the doc_id for citations.

    2. Hybrid retrieval. Run keyword (BM25) and vector search and take the union. Keyword catches the exact strings vectors miss; vectors catch the meaning keywords miss. Neither alone is enough in the enterprise, where half the questions hinge on a specific identifier.

    3. Rerank. Score the union with a cross-encoder that reads the query and each chunk together. This is the highest-ROI line most teams aren’t running — it reorders “nearby” into “actually relevant.”

    4. The grounding gate (relevance floor). The seam. If nothing clears a relevance threshold, return nothing. This is where your operating DNA — what the system is allowed to answer, and from what — stops being a hopeful prompt and becomes code. An empty retrieval result is a feature: it’s what lets the generator refuse instead of improvise.

    5. Grounded generation. Instruct the model to answer only from the provided context and cite a doc_id for every claim. With no context, it refuses — by construction, not by good behavior.

    6. Eval & observability. Score every answer for groundedness and citation validity, continuously. Without this you can’t tell an improvement from a regression — you’re just guessing with a chatbot.

    The grounding gate, in code

    Hybrid recall, rerank, and a relevance floor — the floor is the part that makes refusal possible:

    def retrieve(self, query, k=5):
        # Hybrid recall: keyword catches exact terms, vectors catch meaning.
        candidates = self._dedupe(
            self.bm25.search(query, k=20) + self.vector_index.search(query, k=20)
        )
        # Rerank the union with a cross-encoder — the single biggest quality win.
        for c in candidates:
            c.score = self.reranker.score(query, c.text)
        candidates.sort(key=lambda c: c.score, reverse=True)
        # Relevance floor: if nothing is truly relevant, return nothing.
        # An empty result is a feature — it lets the generator refuse.
        return [c for c in candidates[:k] if c.score >= self.min_score]

    And the grounding instruction that turns “no context” into “no answer”:

    GROUNDING_INSTRUCTION = (
        "Answer ONLY from the provided context. Cite the doc_id for every claim. "
        "If the context does not contain the answer, say 'I don't have that in the "
        "sources' — do not use prior knowledge."
    )

    Measuring whether it’s actually grounded

    Two numbers predict trust: is every claim supported by a retrieved chunk, and does every cited doc_id actually appear in the context? Score them on a fixed set of questions on every prompt, model, or index change.

    def score(case):
        groundedness = case.claims_supported / case.claims_total
        hallucinated = [d for d in case.cited_doc_ids if d not in case.context_doc_ids]
        return {"groundedness": groundedness, "citation_valid": not hallucinated}

    The guardrail everyone forgets: retrieval-time access control

    Every architecture above assumes one reader is allowed to see every document. In a real enterprise they are not. HR files, board decks, a customer’s data walled off from another customer’s — the same index holds chunks that different users have different rights to. Miss this and you haven’t built a helpful assistant; you’ve built the most efficient data-exfiltration path in the company. The fastest way to leak the salary spreadsheet is to embed it and let anyone ask.

    The critical design decision: access control lives in retrieval, not in the prompt. Filter the candidate set to what this caller is entitled to see before anything is reranked or shown to the model — sourced from the same entitlements as the system of record (row-level security, document ACLs), not re-invented in the pipeline.

    def retrieve(self, query, user, k=5):
        candidates = self._dedupe(
            self.bm25.search(query, k=40) + self.vector_index.search(query, k=40)
        )
        # Trim to what THIS user may see — enforced at the data layer, BEFORE rerank,
        # so unauthorized chunks never influence ranking or reach the model at all.
        candidates = [c for c in candidates if can_read(user, c.doc_id)]
    
        for c in candidates:
            c.score = self.reranker.score(query, c.text)
        candidates.sort(key=lambda c: c.score, reverse=True)
        return [c for c in candidates[:k] if c.score >= self.min_score]

    A system prompt that says “only answer if the user is authorized” is not a security control — it’s a polite request to a probabilistic system, and it will eventually say yes. Real access control is a hard filter on the retrieval set, applied before rerank so an unauthorized chunk can’t even tilt the ranking. This is the RAG expression of least privilege — the same operating DNA that governs a good agent: the model may ground only on what this specific user could already open for themselves. That’s a decision you enforce in code, not one you hope the model respects.

    The parts everyone underestimates

    Chunking is a retrieval decision. More RAG quality is won or lost here than in model choice. Test chunk sizes against your eval set like any other parameter.

    Reranking is the cheap win you’re skipping. A cross-encoder over the top ~40 candidates typically moves groundedness more than a model upgrade — for a fraction of the cost.

    The relevance floor is the whole ballgame. “Always answers” is the root of hallucinated RAG. A threshold that returns nothing when nothing is relevant is what converts a plausible liar into a trustworthy assistant.

    Retrieval decays silently. A doc gets superseded, and the old chunk still ranks — confidently. Freshness and provenance on your index matter more than the model you pick.

    The takeaway

    RAG trustworthiness is a retrieval-and-grounding problem, not a model problem. What the system is allowed to answer — and from what — is operating DNA, and it belongs in code at the grounding gate, not in a prompt you hope the model respects. Get that seam right and you can swap models freely as they improve. Skip it and you’ve built a very persuasive way to cite the wrong document.

    The retriever and the groundedness eval from this post are runnable on GitHub: github.com/waghmaredb/vexpose-labs. Building enterprise RAG and fighting the same failure modes? I’d like to compare notes — reach me on LinkedIn or X.

  • The ServiceNow REST pagination gotcha that silently drops records

    ServiceNow’s REST API uses offset-based pagination. When you paginate through a live table—one where records are being created or deleted—the offset becomes stale between calls. Records silently get dropped.

    Why it happens

    The offset is positional within the current result set at the exact moment you query. If a new incident is created and sorts before your current page, all subsequent offsets shift down—you skip records.

    Example: You’re processing incidents with offset=0, limit=100, then offset=100, limit=100. If a new incident arrives and sorts into position 50, the second page now contains records 151–250 instead of 100–200. Record 100 is never seen.

    The fix: use sys_id as a cursor

    Instead of offset, anchor pagination to sys_id. Since sys_id is immutable, new records don’t invalidate your position—they sort after your cursor.

    # DON'T: offset drifts
    offset = 0
    while True:
        records = api.query(offset=offset, limit=100)
        process(records)
        offset += 100  # Invalid if records were added
    
    # DO: stable cursor
    last_sys_id = ""
    while True:
        query = f"sys_id>{last_sys_id}^ORDERBYsys_id" if last_sys_id else "ORDERBYsys_id"
        records = api.query(query, limit=100)
        process(records)
        if records:
            last_sys_id = records[-1]["sys_id"]  # Advance cursor
    

    See the full before-and-after in github.com/waghmaredb/vexpose-labs.

  • The Benchmark Trap: Why Your Storage Numbers Lie — and How to Get Honest Ones

    A vendor datasheet promises a million IOPS. You buy the array, point your workload at it, and it’s slow. The number wasn’t a lie — it was just irrelevant to you. That gap, between an impressive benchmark and a disappointing production system, is where a lot of infrastructure money quietly goes to die.

    I’ve spent a large part of my career with fio, vdbench, and HammerDB open in front of me. The tools are easy. Getting an honest number out of them is not. Here’s the field guide I wish more teams had before they trusted a benchmark — their own or a vendor’s.

    The number isn’t wrong. It’s answering a different question.

    Peak IOPS on a datasheet is a real measurement — of a workload that looks nothing like yours. Tiny block size, unlimited queue depth, a working set small enough to live entirely in cache, reads only, measured for ten seconds. Your production is a 70/30 read-write mix, 16K blocks, a working set far larger than cache, and it has to stay fast at 2 a.m. on day 400. Same tool, different universe. Before you argue about whose array is faster, make sure you’re both describing the same planet.

    The traps that inflate a benchmark

    Most misleading numbers come from a short list of mistakes:

    • The working set fits in cache. You benchmarked DRAM, not the media. Size the dataset several times larger than the controller cache or you’re measuring the wrong component.
    • No steady state. Flash gets slower once garbage collection kicks in. A 60-second run flatters an SSD that looks very different an hour later. Precondition the device, then measure.
    • Queue-depth theater. Cranking queue depth to 256 maximizes IOPS and obliterates latency. It produces a big number and an unusable response time.
    • Wrong block size or mix. The datasheet uses 4K reads; your database does 8K–16K with real writes. Model the actual mix or the result is fiction.
    • A single sample. One run is an anecdote. The variance across five runs is the actual story.

    An honest fio job to start from

    [global]
    ioengine=libaio
    direct=1
    runtime=600
    time_based=1
    ramp_time=60              # reach steady state before recording
    group_reporting=1
    
    [db-like]
    rw=randrw
    rwmixread=70              # 70/30 read-write, like an OLTP database
    bs=16k                    # your real block size, not 4k
    iodepth=32               # realistic, not a vanity QD of 256
    numjobs=4
    size=200g                # larger than the array's cache
    percentile_list=99:99.9  # report the tail, not just the average

    Every knob here is a decision about honesty. direct=1 bypasses the page cache so you measure storage, not memory. ramp_time throws away the artificially fast warm-up. size forces cache misses. percentile_list is the one most people skip — and it’s the one that matters most.

    Read the right metrics

    Peak IOPS is the vanity metric. What actually predicts whether production will be happy:

    • Latency at your target throughput — not throughput at unlimited latency. Those are different questions with very different answers.
    • Tail latency (p99, p99.9). Your users feel the worst 1% of requests, not the average. A great mean with an ugly tail is a bad system wearing a good costume.
    • Consistency over time. Does it hold at steady state, or degrade as the device fills and ages?

    One reframing kills most bad purchases: fix a latency budget — say, 1 ms at p99 — and ask “how many IOPS can it sustain at or under that?” Suddenly the million-IOPS array and the “slower” one often trade places.

    Why this is a leadership discipline, not a lab chore

    Benchmarking is how you replace opinion with evidence. In a room full of vendor claims and strong personalities, the person holding a reproducible number wins the decision — and deserves to. That’s also why a benchmark is a conviction with a review date: you state a performance expectation strongly enough to plan around it, and you re-run it when the firmware, the workload, or the scale changes. A claim without a benchmark is just an opinion. A benchmark you can’t reproduce is just a different opinion with a chart attached.

    The takeaway

    Don’t ask “how fast is it.” Ask “how fast is it, running my workload, at my latency budget, at steady state, averaged over five runs.” The tools will answer honestly if you ask honestly. Everything else is marketing with a monospaced font.

    The full fio job file is on GitHub: github.com/waghmaredb/vexpose-labs. If you benchmark enterprise storage or databases for a living, I’d like to compare methodologies — reach me on LinkedIn or X.

  • vExpose is back — and it’s changing

    A quick, honest note: vExpose went quiet for a while. I’m back — and the blog is changing in a way I’m genuinely excited about.

    Here’s the shift. Most technical writing shows you the strategy but hides the wiring. Going forward, vExpose does both: I state the call a technology leader has to make, then prove it in code, architecture, and benchmarks. One conviction runs through all of it — AI fails in the operating model, not the model. The tool is not the transformation.

    Two things to start with:

    • A new flagship piece, A Reference Architecture for Agentic ITSM, on safely letting a GenAI agent act in ServiceNow — with the guardrail code that makes it work.
    • A new Start Here page that organizes the best of the blog by topic, so you can jump straight to what’s useful.

    What’s coming: reference architectures, build logs with real code, and benchmarks — on a steady cadence this time. No noise, just the work. If there’s a topic you want me to dig into, just reply — I read everything.

    — Deepak

  • A Reference Architecture for Agentic ITSM: Wiring GenAI into ServiceNow Without Losing Control

    A year ago I wrote about integrating generative AI into ITSM workflows. Back then, “integration” mostly meant a chatbot that summarized tickets and drafted responses. Useful, but bounded — the model suggested, a human acted.

    That line has moved. The interesting question in 2026 isn’t whether GenAI can read a ticket; it’s whether you can safely let it close one. The moment a model can take actions in your system of record — reassign an incident, run a diagnostic, restart a service, request a change — you’ve crossed from “assistant” into “agent.” And agents in production are an infrastructure and governance problem long before they’re a model problem. This is the clearest case I know of a principle I keep coming back to: AI fails in the operating model, not the model.

    This post is the reference architecture I’d stand up to do agentic ITSM properly: enough autonomy to actually save time, enough control that I’d let it near a production ServiceNow instance. I’ll show the components, the wiring, the code that matters, and — more importantly — the parts everyone underestimates.

    The failure mode we’re designing against

    Most agentic pilots fail the same way. Someone connects a capable model directly to an API with broad credentials, it works beautifully in the demo, and then in week three it confidently takes a wrong action on a real incident because a retrieved document was stale and nothing stopped it. The problem was never the model’s intelligence. It was that the architecture had no seam between “the model decided” and “the system did.”

    Which is the whole point in one line: the tool is not the transformation. A more capable model doesn’t close this gap — it just takes the wrong action more fluently. Everything below exists to create that seam.

    The reference architecture

    Five layers, each with a single responsibility. The discipline is keeping them separate — that separation is what makes the system auditable, testable, and safe to evolve.

      Trigger  (new/updated incident, chat, webhook)
         |
         v
      Retrieval Layer      RAG over KB, CMDB, past incidents
         |
         v
      Orchestration Layer  agent loop + tool-calling
         |
         v
      Policy & Guardrail Gate  --- restricted / low confidence --->  Human-in-the-loop
         |                                                            (approval queue)
      allowed + high confidence                                            |
         |                                                              approved
         v  <-------------------------------------------------------------+
      Action Layer  (scoped ServiceNow API)
         |
         v
      Observability  (traces, evals, audit log)  --->  back to Trigger

    1. Retrieval Layer. The agent is only as good as what it knows about this incident. Ground every decision in your own data: the knowledge base, the CMDB (what is this CI, what depends on it), and — the highest-signal source most teams ignore — the resolution history of similar past incidents. Retrieval quality, not model size, is usually the difference between a useful agent and a plausible-sounding one.

    2. Orchestration Layer. The agent loop: given the incident and retrieved context, decide the next step, optionally call a tool, observe the result, repeat until resolved or escalated. This is where model-provider choice lives — and it should be swappable, not hardcoded.

    3. Policy & Guardrail Gate. The seam. Every proposed action passes through a deterministic policy check before it can touch ServiceNow. This layer is plain code, not a model — because “what is this agent allowed to do” is a decision you must be able to read, test, and audit, not one you delegate to a probability distribution. This is where your operating DNA — how decisions get made and who is trusted to act — stops being culture and becomes code.

    4. Action Layer. A narrow, scoped adapter to ServiceNow. It exposes only the specific operations the agent is permitted to perform, each backed by a least-privilege service account. The agent never gets your admin token.

    5. Observability. Full traces of every decision, tool call, and action; an immutable audit log; and continuous evaluation. If you can’t answer “why did the agent do that, and was it right,” you can’t run this in production.

    The seam, in code

    Here’s the heart of it: the orchestration layer proposes an action, and the guardrail gate decides whether it executes, gets downgraded to a human approval, or is refused. This is deliberately boring, deterministic code — that’s the point.

    from dataclasses import dataclass
    from enum import Enum
    
    class Decision(Enum):
        ALLOW = "allow"
        REQUIRE_APPROVAL = "require_approval"
        DENY = "deny"
    
    @dataclass
    class ProposedAction:
        name: str            # e.g. "restart_service", "reassign", "add_work_note"
        target_ci: str       # configuration item the action touches
        params: dict
        confidence: float    # model-reported, treated as a hint, never as truth
    
    # Policy is data you can read, diff in git, and audit — not model output.
    POLICY = {
        "add_work_note":   {"max_blast_radius": "none",   "min_confidence": 0.0},
        "reassign":        {"max_blast_radius": "ticket", "min_confidence": 0.75},
        "restart_service": {"max_blast_radius": "service","min_confidence": 0.90,
                            "require_approval_for": ["prod"]},
        # default-deny: anything not listed cannot run
    }
    
    def evaluate(action: ProposedAction, ci_env: str) -> Decision:
        rule = POLICY.get(action.name)
        if rule is None:
            return Decision.DENY                      # default deny — the safe default
        if action.confidence < rule["min_confidence"]:
            return Decision.REQUIRE_APPROVAL
        if ci_env in rule.get("require_approval_for", []):
            return Decision.REQUIRE_APPROVAL
        return Decision.ALLOW

    Three design choices are doing all the work here:

    • Default deny. An action the policy doesn’t explicitly know about cannot run. New capabilities are added deliberately, in a reviewed change to the policy, not discovered by the model at runtime.
    • Confidence is a hint, never a gate on its own. A model saying it’s 0.99 sure is not evidence. Confidence lowers autonomy when low; it never grants autonomy on its own. High-blast-radius actions in prod route to a human regardless.
    • Blast radius is explicit. Every action is classified by what it can break. That classification, not the model’s eloquence, determines how much oversight it gets.

    The action layer itself stays thin and scoped:

    import os, requests
    
    class ServiceNowAdapter:
        """Exposes ONLY whitelisted operations, backed by a least-privilege account."""
        def __init__(self):
            self.base = os.environ["SN_INSTANCE_URL"]
            self.auth = (os.environ["SN_AGENT_USER"], os.environ["SN_AGENT_TOKEN"])
    
        def add_work_note(self, sys_id: str, note: str):
            return self._patch(sys_id, {"work_notes": note})
    
        def reassign(self, sys_id: str, group: str):
            return self._patch(sys_id, {"assignment_group": group})
    
        def _patch(self, sys_id: str, body: dict):
            r = requests.patch(f"{self.base}/api/now/table/incident/{sys_id}",
                               json=body, auth=self.auth, timeout=10)
            r.raise_for_status()
            return r.json()

    Notice what’s not here: there’s no generic “run arbitrary query” or “execute” method. The agent’s power is defined by the surface area of this class, and that surface area is a design decision you make on purpose.

    Deploy it like infrastructure, not like a notebook

    The fastest way to lose trust in an AI system is to run it as a hand-configured process someone SSHed in to start. Treat the whole stack as infrastructure-as-code from day one — reproducible, version-controlled, and reviewable. The service account, its exact permissions, the policy file, and the deployment all live in git:

    # ansible: provision the least-privilege ServiceNow integration account
    - name: Ensure agent service account has ONLY scoped roles
      vars:
        agent_roles:
          - itil            # read/update incidents
          - snc_read_only   # broad read, no write
      ansible.builtin.uri:
        url: "{{ sn_instance }}/api/now/table/sys_user_has_role"
        method: POST
        user: "{{ sn_admin_user }}"
        password: "{{ sn_admin_pass }}"
        body_format: json
        body:
          user: "{{ agent_sys_id }}"
          role: "{{ item }}"
      loop: "{{ agent_roles }}"
      # No admin, no security_admin. If the agent needs more, that's a reviewed PR.

    The point isn’t Ansible specifically — it’s that “what can this agent touch” should be a diff someone approved, not tribal knowledge.

    The parts everyone underestimates

    Evaluation is the hard part, not orchestration. Wiring an agent loop is a weekend. Knowing whether it’s getting better or worse over time is the real engineering. Build a regression set of real (anonymized) incidents with known-good resolutions and run it on every prompt or model change. Without this, every “improvement” is a guess.

    Retrieval decay will hurt you silently. A KB article gets superseded, a CMDB relationship changes, and the agent starts grounding decisions in stale truth — confidently. Freshness and provenance on retrieved context matter more than the model you pick.

    Cost is an architecture decision. Running a frontier model on every ticket update is how pilots die in the budget review. Route by difficulty: cheap/local models for triage and summarization, the expensive model only for genuinely ambiguous cases. Inference routing belongs in the design, not the invoice.

    Human-in-the-loop is a feature, not a fallback. The approval queue isn’t the system admitting defeat — it’s the mechanism that lets you start with tight autonomy and earn more as the eval data proves the agent is trustworthy for a given action class. Every autonomy level you grant is a conviction with a review date: stated strongly enough to act on, dated explicitly enough that your eval data is allowed to revise it.

    How I’d actually roll this out

    1. Shadow mode first. The agent proposes actions and writes them to work notes, but executes nothing. You get real eval data at zero risk.
    2. Graduate low-blast-radius actions. Let it add work notes and reassign autonomously once shadow-mode accuracy clears your bar.
    3. Keep high-blast-radius actions human-gated — indefinitely, if that’s what the risk math says. But don’t hide behind the gate forever, either: shadow mode with no graduation path is its own failure mode — professional neutrality, saying a great deal while committing to nothing. The discipline is interpretive courage: reading the eval data everyone can see and making the call to graduate an action when it has earned it.
    4. Instrument everything from step one. You cannot retrofit observability onto an incident you can’t explain.

    The takeaway

    Agentic ITSM is not a model you buy; it’s a system you architect — because, again, AI fails in the operating model, not the model. The model is the least differentiated part — it’s swappable, and it’s improving whether you do anything or not. Your durable advantage is the seam: the deterministic, auditable, least-privilege layer between what the agent decides and what your systems actually do. Get that right and you can adopt every model improvement safely for years. Skip it and you’ve built a very expensive way to take the wrong action quickly.

    Flexibility with guardrails is the real competitive advantage in enterprise AI. Is your architecture ready to let an agent act — or only to let it talk?

    The policy-gate, ServiceNow-adapter, and provisioning code from this post are runnable on GitHub: github.com/waghmaredb/vexpose-labs. If you’re building agentic workflows on your ITSM stack, I’d like to compare notes — reach me on LinkedIn or X.

  • Integrating Generative AI in ITSM Workflows

    Integrating Generative AI in ITSM Workflows

    IT Service Management (ITSM) teams are increasingly turning to generative AI (GenAI) to streamline workflows, reduce resolution times, and improve user satisfaction. In this post, we’ll explore two architectures for integrating GenAI into platforms like ServiceNow: man-in-the-middle (human-in-the-loop) and agentic automation (autonomous AI agents).


    Why GenAI for ITSM?

    • Ticket overload: Teams handle hundreds of tickets daily, leading to burnout and delays.
    • Knowledge gaps: Agents struggle to find relevant solutions quickly.
    • Repetitive tasks: Manual triage and updates consume valuable time.

    GenAI can automate classification, suggest solutions, and even resolve incidents-but how you integrate it matters. Let’s compare two approaches.


    Architecture 1: Man-in-the-Middle (Human-in-the-Loop)

    In this approach, GenAI acts as an assistant to human agents. It analyzes tickets, suggests actions, and automates tasks but requires human approval before execution.

    Workflow Diagram:

    Use Cases:

    • High-risk scenarios (e.g., critical infrastructure changes).
    • Compliance-heavy environments (e.g., healthcare, finance).

    Implementation Steps:

    1. Set Up ServiceNow Outbound REST Integration (javascript)

    // ServiceNow Scripted REST API (Outbound)
    var request = new sn_ws.RESTMessageV2();
    request.setEndpoint('https://your-middleware.com/process-ticket');
    request.setHttpMethod('POST');
    request.setRequestBody(JSON.stringify(current));
    var response = request.execute();

    2. Build Middleware (Python/Flask Example)

    from flask import Flask, request, jsonify
    import openai

    app = Flask(__name__)
    openai.api_key = "your-api-key"

    @app.route('/process-ticket', methods=['POST'])
    def handle_ticket():
    ticket_data = request.json

    # GenAI analysis
    prompt = f"""
    Classify this ITSM ticket and suggest priority (Critical/High/Medium/Low):
    Title: {ticket_data['short_description']}
    Description: {ticket_data['description']}
    """

    response = openai.ChatCompletion.create(
    model="gpt-4",
    messages=[{"role": "user", "content": prompt}]
    )

    suggestion = response.choices[0].message['content']
    return jsonify({"suggestion": suggestion, "ticket_id": ticket_data['number']})

    3. Human Review & Action
    Agents review GenAI’s suggestions in a dashboard and approve/reject them.


    Architecture 2: GenAI Agentic Automation

    Here, GenAI agents act autonomously within guardrails. They analyze tickets, execute actions (e.g., resolving incidents, updating KBs), and only escalate exceptions to humans.

    Workflow Diagram:

    Use Cases:

    • Low-risk, repetitive tasks (e.g., password resets, FAQ responses).
    • High-volume environments needing 24/7 support.

    Implementation Steps:

    1. Autonomous Ticket Resolution with Python

    import requests
    import openai

    def resolve_ticket_automatically(ticket_id):
    # Fetch ticket from ServiceNow
    snow_url = f"https://instance.service-now.com/api/now/table/incident/{ticket_id}"
    headers = {"Accept": "application/json"}
    auth = ("admin", "password")
    response = requests.get(snow_url, headers=headers, auth=auth)
    ticket = response.json()['result']

    # GenAI analysis
    prompt = f"""
    Resolve this ticket autonomously if possible. Provide a solution and mark as closed.
    Ticket: {ticket['description']}
    """

    ai_response = openai.ChatCompletion.create(
    model="gpt-4",
    messages=[{"role": "user", "content": prompt}]
    )

    # Auto-resolve if confidence is high
    if "reset password" in ai_response.choices[0].message['content'].lower():
    update_data = {
    "state": "6", # Resolved
    "close_notes": "Automatically resolved: Password reset instructions sent."
    }
    requests.patch(snow_url, json=update_data, headers=headers, auth=auth)

    2. Guardrails for Safety

    • Limit permissions (e.g., agents can’t modify user roles).
    • Log all actions for auditing.
    • Escalate tickets containing keywords like “outage” or “data breach” to humans.

    Key Considerations

    Combined Architecture Diagram:


    Best Practices for Both Architectures

    • Data Privacy: Mask PII/PHI in tickets before sending to GenAI APIs.
    • Feedback Loops: Let agents rate AI suggestions to improve models.
    • Tooling: Use frameworks like LangChain for complex workflows.

    Conclusion

    Whether you choose man-in-the-middle or agentic automation depends on your risk tolerance and use case. Start with a hybrid approach: use autonomous agents for simple tasks (e.g., FAQs) and human-in-the-loop for critical workflows. As trust in the system grows, expand automation cautiously.

    GenAI is set to redefine ITSM, propelling organizations from automation to true intelligence. The journey is just beginning-those who embrace this shift will not only optimize IT operations but also unlock new levels of agility, resilience, and innovation. The future of ITSM is not just faster or cheaper-it’s smarter, more adaptive, and profoundly more human.

  • The Evolution of IT Infrastructure: Why Disaggregated Platforms Are the Future for GenAI

    The Evolution of IT Infrastructure: Why Disaggregated Platforms Are the Future for GenAI

    Over the past two decades, enterprise IT architecture has undergone a dramatic transformation. What began as siloed, three-tier environments has evolved through converged and hyper-converged infrastructure (HCI) to today’s cutting-edge disaggregated platforms. This journey is more than a story of hardware innovation-it’s the foundation for the next era of data-driven business, especially as generative AI (GenAI) workloads reshape the technology landscape.

    From Three-Tier to Hyper-Converged: The Drive for Simplicity

    Three-tier architecture-with separate compute, storage, and networking-offered flexibility, but at the cost of complexity and inefficiency. IT teams often faced overprovisioning, stranded resources, and operational headaches.

    The arrival of converged infrastructure bundled these elements into pre-validated stacks, simplifying procurement and deployment. Yet, the fundamental silos remained, limiting agility and resource utilization.

    Hyper-converged infrastructure (HCI) took consolidation further by merging compute and storage into modular nodes managed by software. According to Fortune Business Insights, the global HCI market is projected to reach $65 billion by 2029, reflecting enterprises’ desire for simplified management and scalability. However, HCI’s tightly coupled design makes it difficult to scale compute and storage independently-a critical limitation for today’s AI and data-intensive workloads.

    The Rise of Disaggregated Platforms: Built for GenAI

    Enter disaggregated infrastructure: an architecture that decouples compute, storage, and networking into independent resource pools. This approach is rapidly gaining traction, with the composable/disaggregated infrastructure market expected to triple to over $19 billion by 2029 (MarketsandMarkets).

    Why Disaggregation Matters-Especially for GenAI

    1. Independent Scaling GenAI workloads are unpredictable and data-hungry. Disaggregated platforms allow organizations to scale storage for massive datasets or add GPU-rich compute nodes as needed-without unnecessary overprovisioning.
    2. Superior Resource Utilization Dell reports that disaggregated architectures can deliver “orders of magnitude higher” core utilization and reduce server and software licensing costs by up to 50%.
    3. Performance for AI Technologies like NVMe-over-Fabrics and CXL memory pooling enable low-latency, high-throughput access to data-keeping GPUs saturated and AI pipelines moving at full speed.
    4. Hybrid and Edge Flexibility Disaggregated storage and compute pools can be orchestrated across on-premises, cloud, and edge environments, supporting the distributed nature of modern AI applications.

    Real-World Impact

    Industry leaders are moving fast:

    • Dell’s PowerEdge servers offer modular, independently scalable resources.
    • Many vendors/startups are rapidly evolving their portfolio to embrace disaggregation, making it the new default for enterprises scaling GenAI and other advanced workloads.
    • Edge AI is now viable, with lightweight models running inference on disaggregated clusters far from the data center.

    As Travis Vigil Vigil, SVP at Dell, puts it: Disaggregation isn’t just about hardware-it’s about rethinking the entire data center ecosystem to unlock performance and control.

    Looking Ahead

    The shift to disaggregated infrastructure is not just a technological trend-it’s a strategic imperative for organizations embracing GenAI and next-generation workloads. By enabling independent scaling, maximizing resource efficiency, and supporting hybrid deployment models, disaggregated platforms are redefining what’s possible in the data center.

    In the age of AI, flexibility is the ultimate competitive advantage. Is your infrastructure ready?

    Let’s connect and discuss how your organization can leverage disaggregated architectures to accelerate GenAI and future-proof your digital transformation.