Network Controllers
Asynchronous Controller Workflows: Tasks and Polling
What you'll learn
- Why a controller returns 202 Accepted with a task ID instead of an immediate result
- How to poll a task until it reaches a final state
- That a task can be accepted and later fail — a genuinely new failure mode
- How this extends Phase 3.1's 'HTTP success ≠ data success' into 'accepted ≠ completed'
Listen to a guided explanation of this lesson.
Spoken narration isn't supported in this browser.
Accepted is not completed
Phase 3.1 taught that a 200 response doesn't guarantee the data is correct. A controller's task workflow adds a sharper version of the same lesson: submitting a change doesn't even guarantee the work is done yet — only that the controller has accepted responsibility for doing it.
- Submit desired statePOST /tasks
- 202 Accepted + task IDqueued — not done
- Poll the taskGET /tasks/{task_id}
- runningstill not done
- completed or failednow, and only now, is it resolved
Submitting a task, for real
import requests
response = requests.post(
"http://127.0.0.1:8766/tasks",
json={"device_id": "SW2", "type": "vlan_config"},
headers={"Authorization": "Bearer demo-token"},
timeout=2,
)
print(response.status_code)
print(response.json())The Authorization header pattern is identical to Phase 3.1's — not re-taught, just reused. json= sends the desired-state intent as a body, the same requests mechanic from Phase 3.1's mutation lesson.
202
{'task_id': 'task-SW2-1', 'device_id': 'SW2', 'status': 'queued'}Real, verified output. status_code is 202, not 200 — and the body's own status field says 'queued,' not a result. Both signals agree: this is not a finished answer.
Polling to a real, final state
import time
def poll_task(task_id):
for _ in range(10):
response = requests.get(f"http://127.0.0.1:8766/tasks/{task_id}", timeout=2)
body = response.json()
print(body)
if body["status"] in ("completed", "failed"):
return body
time.sleep(0.5)
poll_task("task-SW2-1")A bounded poll loop — the same 'don't wait forever' discipline as timeout= in Phase 3.1, applied here as a maximum number of polling attempts. time.sleep() between polls is the same pattern Reliable Automation used between retries.
{'task_id': 'task-SW2-1', 'device_id': 'SW2', 'status': 'running'}
{'task_id': 'task-SW2-1', 'device_id': 'SW2', 'status': 'completed', 'changed': True}Real, verified output — SW2's task genuinely took two polls to resolve: running, then completed. changed: True means the controller actually made a change, not just confirmed one was already in place.
The failure path: accepted, then genuinely failed
response = requests.post(
"http://127.0.0.1:8766/tasks",
json={"device_id": "SW4", "type": "vlan_config"},
headers={"Authorization": "Bearer demo-token"},
timeout=2,
)
print(response.status_code, response.json())
result = poll_task(response.json()["task_id"])
print(result)SW4's task is genuinely accepted (202) — the controller has no way to know at submission time that the device won't actually be reachable.
202 {'task_id': 'task-SW4-1', 'device_id': 'SW4', 'status': 'queued'}
{'task_id': 'task-SW4-1', 'device_id': 'SW4', 'status': 'running'}
{'task_id': 'task-SW4-1', 'device_id': 'SW4', 'status': 'failed', 'reason': 'device unreachable from controller'}Real, verified output. The 202 was completely genuine — the controller really did accept the task. The failure only became knowable later, by polling — exactly the gap this lesson exists to close.
A genuinely new failure mode
Every failure Phase 3.1 taught (404, 401, a request timeout) was knowable from the response to the exact request that triggered it. A task's failure is knowable only later, from a separate request (polling) — and can happen for reasons (like a device going unreachable on the controller's southbound side) that have nothing to do with whether the original POST /tasks request itself was well-formed.
Key takeaways
- 202 Accepted with a task ID means the controller accepted the request — not that the change is complete.
- GET /tasks/{task_id}, polled with a bounded loop and a delay between attempts, is the only way to learn a task's real, final outcome.
- A task can be genuinely accepted and later genuinely fail — a failure mode with no Phase 3.1 equivalent, since direct REST failures were always knowable immediately.
- This extends Phase 3.1's 'HTTP success ≠ data success' into 'accepted ≠ completed' — the same discipline, one level further from the original request.
Done with this lesson?
Next lesson
Policies, Templates, and Controller-Driven Change