Network Controllers
Desired State vs. Observed State: Controller-Driven Validation
What you'll learn
- The difference between desired state and observed state
- Why a completed task status is a claim, not proof
- How to check observed state independently, as its own request
- How this generalizes Reliable Automation's post-check to controller architecture
Listen to a guided explanation of this lesson.
Spoken narration isn't supported in this browser.
Two different things, easy to conflate
Desired state is what you asked for. Observed state is what the controller has actually, separately confirmed is true right now. A task reporting completed is the controller's claim that it did the work — observed state is the independent check of whether that claim holds.
- What was requestedthe intent submitted in Lesson 5
- A target, not a confirmation
- What the controller has confirmed is actually truequeried separately, GET /devices/{id}/observed-state
- The only thing that actually proves a change stuck
A task can report completed and still be wrong
# SW5's task from Lesson 4/5's pattern has already resolved:
# {'status': 'completed', 'changed': True}
response = requests.get("http://127.0.0.1:8766/devices/SW5/observed-state", timeout=2)
print(response.json())A completely separate request from the task poll — this is the point. Nothing about polling a task to 'completed' automatically checks the device's real state; that has to be asked for explicitly.
{'vlans': [], 'last_synced': '2026-08-26T09:00:00Z', 'hostname': 'SW5'}Real, verified output. SW5's task reported completed with changed: True — but its observed state shows an empty VLAN list, no VLAN 10 in sight. The task's own claim and the actual, separately-checked state disagree.
Checking both, in order
def check_observed_state(hostname, required_vlans):
response = requests.get(f"http://127.0.0.1:8766/devices/{hostname}/observed-state", timeout=2)
actual_vlans = response.json()["vlans"]
missing = [v for v in required_vlans if v not in actual_vlans]
return len(missing) == 0, missing
verified, missing = check_observed_state("SW5", [{"id": 10, "name": "USERS"}])
print("verified:", verified, "missing:", missing)Only run once a task has already resolved to completed — checking observed state before that would be premature. This is the same 'check, don't just trust' shape as validate_rendered_config() from Configuration & Template Automation, now checking a controller's reported reality instead of rendered text.
verified: False missing: [{'id': 10, 'name': 'USERS'}]Real, verified output — proving the mismatch programmatically, not just by eye.
Generalizing Reliable Automation's post-check
apply_ntp_config() in Reliable Automation read the device's own state again after a change, independently of whether the change call itself reported success — because a change call succeeding was never proof the change actually took effect. Observed state is that exact discipline, now applied to a controller's task claims instead of a direct device response.
Key takeaways
- Desired state is what was requested; observed state is what the controller has separately confirmed is actually true right now.
- A task's completed status is the controller's own claim — it is not, by itself, proof the change took effect.
- GET /devices/{id}/observed-state is a genuinely separate request, checked only after a task resolves — verified directly to disagree with a completed task for SW5.
- This generalizes Reliable Automation's post-check discipline (Phase 2.8) to controller architecture — never trust a completion claim without an independent check.
Done with this lesson?
Next lesson
Capstone: Automating a Network Through a Controller