Skip to content

Part 3 of 3 · hands-on

Alerts, automation and AI

One alert, four possible paths, and the decisions that still need a person.

An alert fires and a workflow handles it. Follow each step: receive the alert, gather facts, apply fixed rules, and take action. Then mark the device as under maintenance and see why the result changes. An optional AI step writes a root-cause summary, but it never chooses the action.

Alertmanager → webhook → Prefect → Infrahub Fixed rules, optional AI summary Ends with a proceed/skip comparison and reflection

Late morning. The clock is creeping toward lunch. The flap-rate panel from before the break is still pinned in a tab. You're both finishing coffee when a BgpSessionNotUp alert lands — a real one, on the lab. Your senior glances at the dashboard, then at you.

"Watch what the workflow handles on its own. Then we'll change the facts and see it make a different choice. By the end, you'll know what the automation can do and what still needs a person."

Follow the live path

The live path is numbered Live 1 through Live 5. Run the commands and read the uncollapsed text in those sections. Browser tours, implementation details, and extra query practice are inside collapsed Optional or Curious? boxes; they are not required to complete the story.

The main route stays in the terminal until the final comparison: observe a real alert, gather its evidence, follow a proceed decision, then turn on maintenance and see the same alert return skip.

Before the live session

Prepare the lab before learners begin. The live path starts with nobs packt reset, not with container startup or configuration.

First time spinning up the lab?

If the lab is stopped, start it and wait until every service says ok:

nobs packt up
nobs packt status

Loki may be the last service to turn green.

If you landed straight on Part 3 without running Quickstart, seed Infrahub once after the stack is ready. Infrahub stores the intended network state that the workflow checks on every alert:

nobs packt load-infrahub

Enable the AI-written summary

RCA means root-cause analysis: a short explanation of the likely cause and what to check next. Enable the offline demo provider now; it needs no API key and costs nothing:

Edit workshops/packt/.env and set:

ENABLE_AI_RCA=true
AI_RCA_PROVIDER=demo

Then reload the workflow container so the new env takes effect (a plain docker compose restart won't pick up .env changes):

nobs packt up

The demo provider fills a template with the facts gathered by the workflow. It does not call an AI service, but it lets you see where an AI-written summary would appear. A proceed decision writes the summary to Loki with ai_rca="true". The Take it home page shows how to use a real provider.

What's a workflow?

A workflow is a set of steps started by an event. Here, Alertmanager sends a new alert to Prefect. Prefect then gathers facts, applies fixed rules, and decides whether to silence the alert.

The handoff at a glance:

   alert fires
        │
        ▼
   Alertmanager
        │
        ▼
   webhook receiver
        │
        ▼
   Prefect workflow
        ├── gather evidence
        ├── make a decision
        └── take or skip action

Prefect shows the three steps as evidence, policy, and action. Here, policy simply means the fixed decision rules: the same facts always produce the same result. The optional AI step writes a summary but does not choose the action.

Start the live path

Reset to a known-good baseline. This expires silences from earlier exercises and clears any maintenance flags:

nobs packt reset

Continue to Live 1 to confirm the alerts.

Optional UI check — Workshop Home

Open Workshop Home at http://localhost:3000/d/workshop-home. The Currently firing alerts table shows the same alerts as nobs packt alerts.

Use the Prometheus datasource for metric queries and the Loki datasource for logs and workflow records. Both are available in Grafana Explore.

The cycle — alert → evidence → policy → action

Two BgpSessionNotUp alerts are firing. A small Python workflow handles each one in four steps:

  1. Alert — receive the alert.
  2. Evidence — gather the intended state, current metrics, and recent logs.
  3. Decision rules — apply fixed rules to those facts. Prefect calls this step policy.
  4. Action — silence the alert or leave it alone, then record why.

The Part 3 cycle — alert, evidence, policy, action The Part 3 cycle — alert, evidence, policy, action

Optional preview — inspect the latest workflow run

Run nobs packt cycle srl1 10.1.99.2. The Prefect flow runs panel shows the overall run followed by its three steps:

               Prefect flow runs (last 30m)
┏━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Started  ┃ State     ┃ Flow                            ┃
┡━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ 07:44:14 │ COMPLETED │ quarantine_bgp | srl1:10.1.99.2 │
│ 07:44:14 │ COMPLETED │   ├─ evidence                   │
│ 07:44:15 │ COMPLETED │   ├─ policy                     │
│ 07:44:16 │ COMPLETED │   └─ action                     │
└──────────┴───────────┴─────────────────────────────────┘

The same flow and task names appear in the Prefect UI at http://localhost:4200/runs.

The rest of Part 3 is structured around this cycle:

  • Live 1–4 follow one alert from receipt to action.
  • Live 5 marks the device as under maintenance and runs the same alert again.
  • Optional practice uses Loki to count the decisions after the live path is complete.

An alert tells you that a condition is true. The workflow adds the context needed to decide what to do, then leaves a record for the next person.

Deep dive — the four decision paths in full

This is optional reference material. The exercises below do not depend on it.

Each BgpSessionNotUp alert goes through the same fixed rules. The workflow compares the intended state from Infrahub with the current state from Prometheus. The same inputs always produce the same decision. The AI-written summary comes afterwards and cannot change that decision.

   alert details
        │
        ▼
   evidence  ── intended state + metrics + logs
        │
        ▼
   policy  ── fixed decision rules
        │
        ▼
   one of: proceed · skip · resolved · stop

The usual decisions are proceed, skip, and resolved. The table shows two kinds of skip because “already healthy” and “under maintenance” are different reasons. A rare stop result means the workflow could not find the device in Infrahub.

Every decision is written to Loki as an audit record: one log line containing the device, peer, result, and reason. The optional practice at the end counts those records by decision.

Path Trigger Decision Outcome
Mismatch → proceed Intent says peer up, metrics disagree proceed The flow signals "this needs human attention" — visible in the audit record, plus a silence
Healthy → skip Intent and metrics agree (no real problem) skip Audit record only
In-maintenance → skip Device's maintenance flag is true in Infrahub skip Audit record only
Resolved → audit trail Alert resolved resolved Audit record only

Keeping the two skip reasons separate lets the next operator tell whether the peer recovered or whether planned work prevented action.

The workflow returns stop when Infrahub has no matching device. Without an intended state, it cannot safely choose proceed or skip. This usually means nobs packt load-infrahub has not finished.

Find the records in Loki under {source="prefect", workflow="packt_quarantine_bgp"} or in the Recent events panels on Workshop Home and Device Health.

What does intent actually look like in Infrahub?

The workflow asks Infrahub two questions: should this peer be up? (expected_state) and is this device under maintenance? (maintenance).

GraphQL lets the workflow request only the fields it needs. The nested WorkshopDevice { … bgp_sessions { … } } block asks for a device and its BGP sessions. Infrahub's Sandbox can autocomplete the field names.

You can inspect the same data in two ways:

  1. Infrahub UI: open http://localhost:8000, log in with admin / infrahub, then open Network Device → srl1. The device page shows maintenance; the Bgp Sessions tab shows each peer's expected state.
  2. GraphQL Sandbox: open http://localhost:8000/graphql, paste the query below, and run it.
{
  WorkshopDevice(name__value: "srl1") {
    edges {
      node {
        name { value }
        maintenance { value }
        site_name { value }
        role { value }
        bgp_sessions {
          edges {
            node {
              peer_address { value }
              expected_state { value }
              remote_as { value }
              reason { value }
            }
          }
        }
      }
    }
  }
}

What you get back (trimmed to the broken peer's bgp_session for readability):

{
  "data": {
    "WorkshopDevice": {
      "edges": [{
        "node": {
          "name": { "value": "srl1" },
          "maintenance": { "value": false },
          "site_name": { "value": "lab" },
          "role": { "value": "edge" },
          "bgp_sessions": {
            "edges": [{
              "node": {
                "peer_address": { "value": "10.1.99.2" },
                "expected_state": { "value": "established" },
                "remote_as": { "value": 65102 },
                "reason": { "value": null }
              }
            }]
          }
        }
      }]
    }
  }
}

Here, maintenance: false means normal checks should continue. expected_state: "established" means the peer should be up. If the metrics disagree, the result is proceed.

What does reality actually look like in the metrics?

The flow asks Prometheus for the current per-peer BGP state — bgp_admin_state, bgp_oper_state, plus the prefix counters. These are the same metric names you queried in Part 1.

Three ways to read the live metrics:

  1. CLI: nobs packt evidence DEVICE PEER shows intended state, metrics, and logs together.
  2. Via Grafana Explore at http://localhost:3000. Pick the Prometheus datasource and run each query separately (one per query row):
    bgp_admin_state{device="srl1", peer_address="10.1.99.2"}
    bgp_oper_state{device="srl1", peer_address="10.1.99.2"}
    bgp_received_routes{device="srl1", peer_address="10.1.99.2"}
    bgp_prefixes_accepted{device="srl1", peer_address="10.1.99.2"}
    
  3. Prometheus HTTP API, for scripts:
    curl -sG 'http://localhost:9090/api/v1/query' \
      --data-urlencode 'query=bgp_oper_state{device="srl1",peer_address="10.1.99.2"}'
    

What you'll see on the broken peer:

bgp_admin_state       = 1   (enable)
bgp_oper_state        = 5   (active — retrying)
bgp_received_routes   = 0
bgp_prefixes_accepted = 0

In plain language: the session is enabled but stuck trying to connect, and it has received no routes. Infrahub says it should be established, so the workflow returns proceed.

Why fixed rules, rather than an LLM, choose the action

The policy is a short if / elif chain in workshops/packt/automation/workshop_sdk.py. Fixed rules are useful here because they are:

  • Predictable: the same facts produce the same result.
  • Replayable: an old alert can be checked again later.
  • Reviewable: policy changes are ordinary code changes.
  • Explainable: the on-call can see exactly which rule matched.
What's a maintenance window — and how does it differ from a silence?

A maintenance window is a flag in Infrahub. It tells the workflow that changes on this device are expected, so it returns skip.

A silence is a temporary mute in Alertmanager. The workflow creates one after a proceed decision so the same alert does not notify repeatedly during the investigation.

Maintenance changes the decision. A silence is an action taken after the decision.

Walk the cycle

In a hurry, or returning later? The cheat-code at the end repeats the whole cycle with six commands.

Live 1 · Alert — see it fire

You are here — Alert: confirm that the alert and its labels arrived You are here — Alert: confirm that the alert and its labels arrived

The workflow can't do anything until an alert exists. Start here: confirm the lab has alerts to work with.

nobs packt alerts

You need the two BgpSessionNotUp rows shown below. Interface alerts may also be present by now, but they are not used in this part:

| Alertname       | Severity | Device / target   | State  | Age |
| BgpSessionNotUp | warning  | srl1 → 10.1.99.2 | firing | ... |
| BgpSessionNotUp | warning  | srl2 → 10.1.11.1 | firing | ... |

Three things to notice:

  • The device label — the router the alert is about (srl1, srl2). The workflow uses this to look up the device in Infrahub.
  • The target label — the peer IP the session is with. In the alert it's called target; the workflow maps it to peer_address when querying Infrahub and Prometheus.
  • The State column. Both rows should read firing. suppressed is also fine; it means a silence is temporarily muting notifications. Live 4 explains it.

If either BGP row is missing, give the alert rule another 60 seconds to evaluate and run the command again.

Optional — see the YAML that triggered BgpSessionNotUp

Prometheus created the alert from this rule in alerting_rules.yml:

- alert: BgpSessionNotUp
  expr: |
    (
      bgp_admin_state{afi_safi_name="ipv4-unicast", name="default"} == 1
    )
    and on (device, peer_address, afi_safi_name, name)
    (
      bgp_oper_state{afi_safi_name="ipv4-unicast", name="default"} != 1
    )
  for: 30s
  labels:
    severity: warning
    workshop: "mno"
  annotations:
    summary: "BGP session not UP ({{ $labels.device }} ↔ {{ $labels.peer_address }})"
    description: |
      admin_state=enable (1) but oper_state is not up (1).
      afi_safi_name={{ $labels.afi_safi_name }}, instance={{ $labels.name }}.

In plain language: the BGP session is configured to be enabled (admin_state == 1), but it is not operationally up (oper_state != 1). and on (...) makes sure both measurements describe the same device and peer. If that mismatch lasts for 30 seconds, the alert moves from pending to firing.

The same alert + suppressed state + silencing ID also shows in the Alert panel of nobs packt cycle srl1 10.1.99.2 — useful if you'll be re-observing this step later.

Optional UI check — Alertmanager

Open Alertmanager at http://localhost:9093/#/alerts to see the same rows with click-to-expand details. Part 2's alert lifecycle covers the pending → firing → suppressed → resolved states.

Curious? See how the alert reaches Prefect
   Prometheus or Loki rule
              │ alert
              ▼
         Alertmanager  ◄── nobs packt alerts
              │ webhook
              ▼
      Prefect alert_receiver
              │
              ▼
   evidence → policy → action

Use --trigger when repeating an exercise

Alertmanager waits 30 minutes before sending the same alert again. This prevents repeated pages, but it is too long for an exercise. nobs packt reset does not reset that timer.

Instead, use:

nobs packt cycle srl1 10.1.99.2 --trigger

--trigger starts a fresh Prefect run immediately. Without it, cycle only shows the current alert, silences, recent runs, and latest decision.

For Part 3, we focus on what happens after the alert is firing: the workflow picks it up and decides what to do. That starts with gathering facts.

Live 2 · Evidence — what the workflow collected

You are here — Evidence: gather intended state, current metrics, and recent logs You are here — Evidence: gather intended state, current metrics, and recent logs

Before choosing an action, the workflow gathers three kinds of evidence:

  • Infrahub: the intended-state database — should this peer be up, and is the device under maintenance?
  • Prometheus: what state is the peer in now?
  • Loki: what happened recently?

Run the same check yourself with one command:

nobs packt evidence srl1 10.1.99.2

(10.1.99.2 is the broken peer on srl1 from Step 1.)

The output is four panels. Each answers a different question:

Panel Source Answers
Source of truth (Infrahub) The intended-state database "Is this peer supposed to be up?"
BGP metrics snapshot (Prometheus) The metrics store "Is this peer actually up?"
Loki — last 20 relevant log lines The log store "What happened recently on this peer?"
Policy hint The workflow's decision rule "What would the workflow decide right now?"
What the four panels actually look like in the terminal

Running nobs packt evidence srl1 10.1.99.2 against the broken peer produces output like this:

╭───────────────────── Source of truth (Infrahub) ─────────────────────╮
│ device          srl1   site=lab  role=edge                           │
│ maintenance     false                                                │
│ intended peer   yes                                                  │
│ expected state  established                                          │
│ reason          ip-mismatch-demo                                     │
│ remote_as       65102                                                │
╰──────────────────────────────────────────────────────────────────────╯
   BGP metrics snapshot (Prometheus)
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━┓
┃ Metric            ┃ Value ┃ Decoded ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━┩
│ admin_state       │     1 │ enable  │
│ oper_state        │     5 │ active  │
│ received_routes   │     0 │ —       │
│ sent_routes       │    10 │ —       │
│ active_routes     │    10 │ —       │
│ suppressed_routes │     0 │ —       │
└───────────────────┴───────┴─────────┘
╭─────────────── Loki — last 20 relevant line(s) ──────────────────────╮
│ {"timestamp":"...","severity":"warn","message":"BGP neighbor         │
│  10.1.99.2: connection refused — peer not reachable on configured    │
│  subnet",...}                                                        │
│ {"timestamp":"...","severity":"warn","message":"BGP neighbor         │
│  10.1.99.2: configured remote-as 65102 but peer never responded;     │
│  FSM stuck in active",...}                                           │
│ {"timestamp":"...","severity":"info","message":"QUARANTINE applied   │
│  (silence_id=...)",...}                                              │
╰──────────────────────────────────────────────────────────────────────╯
╭──────────────── Policy hint ────────────────╮
│ decision: proceed                           │
│ reason  : SoT expects peer up, but metrics  │
│           show mismatch                     │
╰─────────────────────────────────────────────╯

Read it from top to bottom: Infrahub says the peer should be established, Prometheus says it is still trying to connect, and Loki shows connection failures. The final panel predicts proceed because those facts disagree.

Curious? See how the workflow gathers evidence

The flow starts the Infrahub, Prometheus, and Loki reads together, then waits for all three results:

   ┌── evidence_flow ───────────────────────────────┐
   │                                               │
   │  fetch_sot     ──► Infrahub                   │
   │  fetch_metrics ──► Prometheus                 │
   │  fetch_logs    ──► Loki                       │
   │       │                                       │
   │       ▼                                       │
   │  assemble_evidence ──► policy_flow            │
   │                                               │
   └───────────────────────────────────────────────┘
automation/flows.py — evidence_flow
@flow(flow_run_name="evidence | {device}:{peer_address}")
def evidence_flow(device, peer_address, ...) -> EvidenceBundle:
    sot = fetch_sot_task.submit(device=device, peer_address=peer_address, ...)
    metrics = fetch_metrics_task.submit(device=device, peer_address=peer_address, ...)
    logs = fetch_logs_task.submit(device=device, peer_address=peer_address, ...)
    return assemble_evidence_task(sot=sot, metrics=metrics, logs=logs, ...)

.submit() starts each read without waiting for the previous one. assemble_evidence_task runs after all three have finished.

A run makes that order visible in the task trace:

Task run 'fetch_sot[srl1:10.1.99.2]' - 🔎 [evidence] SoT gate for srl1:10.1.99.2
Task run 'fetch_metrics[srl1:10.1.99.2]' - 🔎 [evidence] BGP metrics snapshot for srl1:10.1.99.2
Task run 'fetch_logs[srl1:10.1.99.2]' - 🔎 [evidence] recent logs for srl1:10.1.99.2
Task run 'assemble_evidence[srl1:10.1.99.2]' - ✅ [evidence] maintenance=False expected_state=established
Task run 'assemble_evidence[srl1:10.1.99.2]' -    metrics={'admin_state': 1.0, 'oper_state': 5.0, 'received_routes': 0.0}
Task run 'assemble_evidence[srl1:10.1.99.2]' -    logs collected: 50 lines

Prometheus — read the current BGP values:

def bgp_metrics_snapshot(self, device, peer_address, ...) -> dict[str, float]:
    queries = self.bgp_queries(device, peer_address, ...)
    return {
        "admin_state": first_prom_value(self.prom.instant(queries["admin_state"])),
        "oper_state": first_prom_value(self.prom.instant(queries["oper_state"])),
        "received_routes": first_prom_value(self.prom.instant(queries["received_routes"])),
    }

Loki — keep recent logs labelled with this device and peer:

def bgp_logs(self, device, peer_address, minutes=10) -> list[str]:
    query = f'{{device="{device}", peer_address="{peer_address}"}}'
    return self.loki.query_range(query, minutes=minutes)

Infrahub — read the intended state:

def bgp_gate(self, device, peer_address, afi_safi) -> dict:
    return self.sot.build_bgp_intent_gate(
        device=device,
        peer_address=peer_address,
        afi_safi=afi_safi,
    )

Decode numeric BGP states for people:

OPER_MAP = {
    0: "unknown",
    1: "established",
    2: "idle",
    3: "connect",
    4: "openconfirm",
    5: "active",
}

def decode_bgp_states(metrics: dict[str, float]) -> dict[str, str]:
    oper = int(metrics["oper_state"])
    return {"oper_state": OPER_MAP.get(oper, str(oper))}

The complete implementation is in workshops/packt/automation/workshop_sdk.py.

The first two panels are the key pair:

  • Source of truth says what should be true — for this peer, expected_state=established means "should be up and exchanging routes."
  • Metrics say what is true — for this peer, oper_state=5 means the BGP session is stuck trying to come up.

The gap between those two is the reason the alert is firing.

Optional UI check — intended state in Infrahub

Open http://localhost:8000, click Log in at the bottom left, and use admin / infrahub. Then open Network Device → srl1 → Bgp Sessions. Peer 10.1.99.2 should show Expected State: Established and Reason: ip-mismatch-demo.

Infrahub WorkshopBgpSession detail for the broken peer 10.1.99.2 Infrahub WorkshopBgpSession detail for the broken peer 10.1.99.2

BgpSessionNotUp says only that a session is down. The intended state, maintenance flag, current metrics, and recent logs determine whether that needs action.

Live 3 · Decision rules — what was decided and why

You are here — Decision: apply the same fixed rules to the gathered facts You are here — Decision: apply the same fixed rules to the gathered facts

Prefect calls this step policy. In plain language, it is a short list of decision rules. The workflow asks two questions in order:

  1. Is the device known, and is it under maintenance?
  2. If normal checks should continue, do the current metrics match the intended state?

For srl1 → 10.1.99.2, Infrahub says the peer should be established, while Prometheus says it is still trying to connect. The result is proceed, with the reason SoT expects peer up, but metrics show mismatch.

Start a fresh workflow run so everyone is looking at the same result:

nobs packt cycle srl1 10.1.99.2 --trigger

In the Most recent decision panel, confirm:

decision  proceed
message   SoT expects peer up, but metrics show mismatch
Curious? See the policy code and task output
automation/flows.py — policy_flow
@flow(flow_run_name="policy | {device}:{peer_address}")
def policy_flow(device, peer_address, evidence, workflow=...) -> Decision:
    decision = evaluate_sot_gate_task(evidence=evidence)
    if decision.decision not in {"stop", "skip"}:
        decision = evaluate_metrics_gate_task(evidence=evidence)
    annotate_decision_task(
        workflow=workflow,
        device=device,
        peer_address=peer_address,
        decision=decision,
    )
    return decision

With maintenance=false, both checks run:

evaluate_sot_gate     → proceed (continue to metrics)
evaluate_metrics_gate → proceed (expected state and metrics disagree)
annotate_decision     → decision=proceed

With maintenance=true, the first check returns skip, so the metrics check is unnecessary:

evaluate_sot_gate → skip (device under maintenance)
annotate_decision → decision=skip

The workflow writes every decision to Loki. This decision record — also called an audit record — is a log line containing the device, peer, decision, and reason. It remains available after the alert disappears.

Optional UI check — inspect the decision in Grafana

Open Grafana, switch to the Loki datasource in Explore, and paste:

{source="prefect", workflow="packt_quarantine_bgp", device="srl1", decision=~"proceed|skip|resolved"} | json

The filter shows only decisions; | json displays the fields inside each record. You should see one line per recent decision for srl1.

Field Value (for the broken peer) What it means
decision (label) proceed The workflow decided to take action
peer_address (label) 10.1.99.2 Which peer this decision was about
message (field) SoT expects peer up, but metrics show mismatch Plain-English reason
timestamp 2026-… When the workflow ran

If the query returns "No data", wait 30–60 seconds and run it again.

Optional reference — what an audit record looks like in Loki

The workflow writes one JSON log line per decision. These examples show a normal proceed result and a stop result when Infrahub has no matching device:

// decision=proceed (the actionable mismatch path)
{
  "timestamp": "2026-05-26T11:15:26.971Z",
  "severity": "info",
  "message": "SoT expects peer up, but metrics show mismatch",
  "labels": {
    "decision": "proceed",
    "device": "srl2",
    "peer_address": "10.1.11.1",
    "source": "prefect",
    "workflow": "packt_quarantine_bgp"
  },
  "fields": {}
}

// decision=stop (would only appear if Infrahub didn't have the device)
{
  "timestamp": "2026-05-26T10:43:31.234Z",
  "severity": "info",
  "message": "device not found in Infrahub",
  "labels": {
    "decision": "stop",
    "device": "srl2",
    "peer_address": "10.1.11.1",
    "source": "prefect",
    "workflow": "packt_quarantine_bgp"
  },
  "fields": {}
}

The labels make records easy to filter and count. The message carries the explanation a person reads.

The three decisions, explained

The workflow can write one of three decisions for any given alert:

Decision When it is chosen What happens next
proceed The source of truth says this peer should be up, but the metrics say it isn't. Something is actually wrong. The workflow takes action (Step 4).
skip Either the device is in a maintenance window, or the peer is healthy according to the metrics. No action needed. The workflow just records that it checked.
resolved The alert has stopped firing on its own (the underlying problem went away). No action needed. The workflow records that it resolved.

All three decisions use the same record format. Step 5 changes one maintenance flag so the same broken peer returns skip instead of proceed.

Why record the decision? A silence says that an alert was muted. The audit record also says why, which is what the next on-call needs.

Live 4 · Action — what proceed actually does

You are here — Action: carry out and record the decision You are here — Action: carry out and record the decision

The last step acts only when the decision is proceed. It creates a temporary silence, records that action, and writes the RCA summary. A skip or resolved decision records why no action was taken.

Curious? See the action code and task output
automation/flows.py — action_flow
@flow(flow_run_name="action | {device}:{peer_address}")
def action_flow(device, peer_address, decision, evidence, ...) -> dict:
    if is_ai_rca_enabled() and decision.decision != "proceed":
        rca_text = ai_rca_skipped_task(decision=decision)
    else:
        rca_text = ai_rca_task(evidence=evidence)

    if decision.decision != "proceed":
        return {"action": "none", "silence_id": None, "ai_rca": rca_text}

    silence_id = quarantine_task(
        device=device,
        peer_address=peer_address,
        minutes=quarantine_minutes,
    )
    annotate_action_task(device=device, peer_address=peer_address, silence_id=silence_id)
    return {"action": "quarantine", "silence_id": silence_id, "ai_rca": rca_text}

A proceed run shows the summary, silence, and action record:

ai_rca         → summary written
quarantine     → alert silenced for 20 minutes
annotate_action → silence ID recorded

A skip run does not create a silence:

ai_rca_skipped → policy decided skip
action         → none

The proceed run creates three results: a temporary silence, an action record, and an RCA summary. The live path checks the silence and summary in the terminal; the full records remain available in the optional UI checks.

Confirm the silence

The workflow asks Alertmanager to silence the alert for 20 minutes. Same kind of silence you created by hand in Part 2's "Create a silence by hand" section — only this one was created automatically, scoped to the specific peer.

To see all silences scoped to this peer (plus the current alert state and recent flow runs) in one shot:

nobs packt cycle srl1 10.1.99.2

The Silences panel should show a new 20-minute silence for this peer.

Optional troubleshooting — why a silence may look shorter than 20 minutes

The silence is created with a 20-minute endsAt, but a few things can shorten the visible duration:

  • nobs packt reset truncates every workshop-related silence to NOW as part of clearing state. After a reset, an Alertmanager UI lookup will show the silence as already-expired (or near it).
  • Re-running the workflow on the same alert (via cycle --trigger or a real Alertmanager re-push) creates a new 20-minute silence. The old one continues to expire on its original timeline; both are visible if you list silences with --show-expired.

If the silence you're looking at reads ~3 minutes instead of 20, you most likely just ran reset — the workflow wrote a 20-minute silence and reset immediately truncated its endsAt.

Run nobs packt alerts — the BgpSessionNotUp row for srl1 → 10.1.99.2 should now show suppressed in the State column.

Optional UI check — inspect the silence in Alertmanager

Open Alertmanager at http://localhost:9093/#/alerts and enable the Silenced filter. Find BgpSessionNotUp for srl1 → 10.1.99.2, expand it, and open its silence. Check three fields:

  • Matchers — alertname=BgpSessionNotUp, device=srl1, peer_address=10.1.99.2. These limit the silence to this alert and peer.
  • Comment — QUARANTINE: SoT expects peer up, but metrics show mismatch. This repeats the decision reason from Live 3.
  • Created by — the workflow itself, not a human.
Troubleshooting — don't see suppressed?

If the row shows firing instead, the previous silence has expired. Alertmanager may wait up to 30 minutes before sending the same alert again, so start a fresh cycle yourself:

nobs packt cycle srl1 10.1.99.2 --trigger

Within ~15 seconds a new 20-minute silence is in place; refresh the Alertmanager page and the row should flip to suppressed.

Why silence rather than fix? This workflow is allowed to reduce repeated notifications, not change a network device. The peer remains broken and still needs investigation.

Optional UI check — inspect the action record in Grafana

The workflow writes a second Loki record describing the action:

QUARANTINE applied (silence_id=<uuid>)

The decision record says why the workflow chose to act. This action record says what it did and includes the silence ID. Query it with:

{source="prefect", workflow="packt_quarantine_bgp", device="srl1"} |~ "QUARANTINE applied"

You should see one row for each proceed run, with the time the silence was created.

You can draw these records as vertical markers on a Grafana dashboard: Edit → Dashboard options → Annotations → New annotation, choose Loki, and use the query above. The marker answers a useful timeline question: when did the workflow act?

Read the root-cause summary

The workflow also writes an RCA summary in plain language. What it writes depends on the decision:

Decision What lands in Loki
proceed A summary covering likely cause, immediate actions, and what to verify next
skip Brief annotation: "AI RCA not run — policy decided skip (reason)"
ENABLE_AI_RCA=false Brief annotation: "AI RCA disabled"

Render the latest narrative as Markdown directly in the terminal:

nobs packt rca srl1 10.1.99.2
Optional UI check — compare the decision and RCA in Grafana

RCA records use ai_rca="true" instead of a decision label. Remove the decision filter to see both record types:

{source="prefect", workflow="packt_quarantine_bgp", device="srl1"} | json

You should see the fixed-rule decision and the RCA summary for the same alert. Both use the evidence gathered in Live 2.

The big idea. Fixed rules choose the action. AI explains the evidence in readable form. Keeping those jobs separate makes the action predictable without giving up a useful summary.

By default, the demo provider fills a fixed template with the evidence. The Take it home page shows how to use OpenAI or Anthropic instead.

What proceed doesn't do

Worth saying out loud, so it doesn't trip you up:

  • It does not fix the underlying problem on the device. The broken peer stays broken until a human (or a separate remediation flow) addresses it.
  • It does not open a ticket or page the on-call directly. In production this is where you'd hook in PagerDuty, OpsGenie, Jira, Slack — in this lab, the silence + action audit + AI narrative is the full chain.
  • It does not decide what to do next. That's a human's job: read the action audit, read the narrative, look at the dashboard, walk the runbook.

proceed mutes repeat notifications, records when that happened, and writes a summary. It does not repair the network.

Live 5 · Maintenance branch — same drill, opposite decision

Back at the Decision step: change one fact, then run the same rules again Back at the Decision step: change one fact, then run the same rules again

You've now walked the cycle once: alert → evidence → policy → action. The workflow saw a real mismatch and decided proceed.

The same alert should not always cause the same action. During planned maintenance, a BGP peer may go down as expected. The workflow needs that context.

Infrahub stores a maintenance flag. Change srl1.maintenance from false to true, then run the same alert again. The result should change from proceed to skip.

Step 1 · Flip the maintenance flag

nobs packt maintenance --device srl1 --state

The --state flag sets srl1.maintenance to true (later we'll use --clear to set it back to false). The CLI confirms:

╭──── WorkshopDevice updated ────╮
│ srl1.maintenance: False → True │
╰────────────────────────────────╯
   The next alert for this device will be SKIPPED by the policy.

The command does two things:

  1. The CLI wrote maintenance=true to srl1's record in Infrahub.
  2. It wrote a Loki record of the change with source="workshop-trigger".

The workflow reads the flag each time it runs, so the next decision uses the new value.

Optional UI check — see the flag in Infrahub

Open Infrahub at http://localhost:8000. If you haven't logged in yet, click Log in in the bottom-left, enter admin / infrahub, and submit. Then click Network Device in the left nav, then click srl1. The maintenance field now reads true.

This is the same field shown in Live 2's evidence panel. It has changed from false to true.

Step 2 · Re-trigger the workflow

Alertmanager may take up to 30 minutes to send this alert again. Start the workflow directly so the new decision arrives within seconds:

nobs packt cycle srl1 10.1.99.2 --trigger

This starts the same workflow directly instead of waiting for Alertmanager's 30-minute resend timer. The command waits for the run and then shows the updated decision.

What's the wrapper doing under the hood?

cycle --trigger sends the alert details directly to Prefect's alert_receiver webhook, the same endpoint Alertmanager uses. The raw command is:

docker compose --project-name packt exec prefect-flows \
  prefect deployment run alert-receiver/alert-receiver \
  --param alertname=BgpSessionNotUp \
  --param status=firing \
  --param 'alert_group={"alerts":[{"labels":{"device":"srl1","peer_address":"10.1.99.2","afi_safi_name":"ipv4-unicast"}}],"groupLabels":{"alertname":"BgpSessionNotUp"},"status":"firing"}'

cycle --trigger builds the alert details, sends them, waits for the new Prefect run, and displays the result. Use the wrapper in the workshop; the raw command is available for scripts outside the lab.

Step 3 · Compare the new decision

When the command finishes, read the Most recent decision panel in the terminal:

decision  skip
message   device under maintenance

The peer is still broken, but the result is now skip because Infrahub says the device is under maintenance.

The alert may still show as suppressed: the earlier 20-minute silence keeps running until it expires. The proof of skip is that this run creates no new silence.

The RCA step also skips because the workflow is not taking action. Read the latest RCA record:

nobs packt rca srl1 10.1.99.2

The newest line explains that the RCA was not run because the device is under maintenance. A real provider would therefore make no paid API call.

Optional UI check — compare proceed and skip in Grafana

Open Grafana, select the Loki datasource in Explore, and run:

{source="prefect", workflow="packt_quarantine_bgp", device="srl1", decision=~"proceed|skip|resolved"} | json

The newest line should show decision=skip and message="device under maintenance". The earlier proceed record remains beside it, so you can compare both outcomes.

To inspect the skipped RCA record, run:

{source="prefect", ai_rca="true", device="srl1"} | json

Don't clear maintenance until you've seen skip

The next step clears the maintenance flag. If you race ahead and clear it before the workflow has actually re-evaluated, it'll re-read maintenance=false and return proceed instead of skip. Confirm the terminal shows skip, then continue.

Step 4 · Clear the maintenance flag

nobs packt maintenance --device srl1 --clear

srl1.maintenance is back to false. The next alert will be evaluated normally — back on the proceed path.

The big idea. Maintenance is context stored in Infrahub, not a special rule hidden in the workflow. One flag changes the decision because the workflow checks that context every time.

Close the live path

Your senior leans back. "Last one's a thinking exercise. Pick any of the paths you just ran and answer this for yourself."

Which path would I trust the AI's narrative on without a second look? Which would I always double-check by hand? Why?

Optional discussion prompts
  • The mismatch-proceed path reflects a real fault. What could happen if its summary is wrong?
  • The healthy-skip path is a no-op. Does the AI narrative add anything for an on-call?
  • The maintenance-skip path depends on the source of truth being right. What if Infrahub's wrong?
  • The resolved path describes something that already happened. Is that safer than asking AI what to do next?

There's no single right answer. The point is that the same tool isn't equally valuable for all four paths, and you should know which is which before you trust the narrative in the heat of an incident.

What you took away

Your senior signs off. "If something fires, gather the facts, decide, act, and leave a record. The optional exercises are on the take-home page whenever you want more practice."

  • A useful alert workflow gathers context before choosing an action.
  • Fixed rules return proceed, skip, resolved, or stop from the same set of facts.
  • Infrahub says what should be true; Prometheus and Loki show what is happening now.
  • A maintenance flag changes the decision. A silence only mutes notifications after a decision.
  • AI writes a summary from the same evidence but does not choose the action.
  • Every decision and action is recorded for later review and counting.
Optional practice — count the workflow decisions

Finish at the Action step by reading and counting the records it created Finish at the Action step by reading and counting the records it created

Without scrolling any dashboard, how many alerts has the workflow handled in the time range you're looking at, broken down by decision?

Every decision lands in Loki with source="prefect", workflow="packt_quarantine_bgp", and a decision label. Build a query that counts those records and groups them by decision.

Two hints if you get stuck:

  • count_over_time({...}[$__range]) counts matching records across the time range selected in Grafana.
  • sum by (decision) (...) produces one total for each decision.

Count both devices, but keep a decision filter so action and RCA records are excluded.

Solution and what your query should return
sum by (decision) (count_over_time({source="prefect", workflow="packt_quarantine_bgp", decision=~"proceed|skip|resolved|stop"}[$__range]))

With Explore set to a range that covers your walk (say "Last 30 minutes"), you should land on something like:

Decision Count
proceed a few
skip a few
resolved maybe a few

Exact counts depend on how many cycles you ran. If you get one combined total, check that the query includes sum by (decision).

The audit trail is not only a list to read. You can count it to answer questions such as, “How many alerts did the workflow act on this morning?”

Cheat-code — the whole Part 3 cycle from the CLI in 6 commands

Use these commands for a live demo or a quick repeat of the cycle. Each output matches one of the steps above.

nobs packt cycle srl1 10.1.99.2                      # Step 1: alert, silence, flow, and decision
nobs packt evidence srl1 10.1.99.2                   # Step 2: intended state, metrics, and logs
nobs packt cycle srl1 10.1.99.2 --trigger            # Steps 3 + 4: proceed and create a fresh silence
nobs packt maintenance --device srl1 --state         # Step 5 setup: mark the device under maintenance
nobs packt cycle srl1 10.1.99.2 --trigger            # Step 5: same alert → skip; no new silence
nobs packt maintenance --device srl1 --clear         # cleanup

The punchline lives in the last cycle output. The Flow runs panel shows both runs with the same three child rows — evidence, policy, action — because the same three blocks ran either way. What changed is the Most recent decision panel: proceed after the third command, skip after the fifth. The Silences panel grows by one row only on the proceed run — the absence of a silence on the skip run is the visible proof that the policy decided not to act. A third side-effect lives in Loki: querying {source="prefect", ai_rca="true"} | json shows the proceed run produced a multi-section narrative, while the skip run wrote "AI RCA not run — policy decided skip…". Same alert, same evidence, opposite decisions, the entire workflow behavior (silence + LLM call + audit trail) flipped by one field in the source of truth.

Live 1–5 explain why each panel changes. The optional UI checks show the same run in Alertmanager, Loki, Infrahub, and Prefect.