This document describes how the Steps Handler — the agent's default Update Manifest Handler — processes a multi-step update manifest as currently implemented. It is intended as a precise reference for agent maintainers and step-handler authors. For a higher-level conceptual introduction, see Multi Step Ordered Execution (MSOE) and the public Microsoft Learn doc on multistep ordered execution.
Source of truth: src/extensions/update_manifest_handlers/steps_handler/src/steps_handler.cpp.
The compiled artifact is libmicrosoft_steps_1.so.
When the agent receives an update with manifest version 4.0 or 5.0, the top-level manifest does not have an updateType; it has instructions.steps. The agent implicitly assigns the type microsoft/steps:1 to the top-level workflow and loads the Steps Handler to process it. The Steps Handler is registered for the following extension-ids in the deviceupdate-agent Debian post-install (see registering-device-update-extensions.md):
microsoft/update-manifestmicrosoft/update-manifest:4microsoft/update-manifest:5
For reference steps, the child workflow is also processed by the Steps Handler — it always uses the default reference-step handler microsoft/steps:1 regardless of the child manifest's own type information (DEFAULT_REF_STEP_HANDLER in steps_handler.cpp).
Every Steps Handler operation works against an ADUC_WorkflowHandle tree:
parent (level 0) ← top-level update manifest
├─ child step 0 (level 1) ← inline or reference step
├─ child step 1 (level 1)
│ ├─ grandchild step 0 ← only when the level-1 step is a *reference step*
│ ├─ grandchild step 1 ← (the referenced child manifest's inline steps)
│ └─ ...
└─ child step N
workflow_get_level(handle)returns0for the top-level workflow,1for steps of the top-level manifest, and so on.workflow_get_step_index(handle)is the step's index within its parent.- Child workflows are created lazily by
PrepareStepsWorkflowDataObject(called at the top of every phase). For inline steps it copies file entities and selected components from the parent. For reference steps it downloads the detached child manifest, parses it, and creates the child workflow from that file. - Children are kept alive for the entire workflow so subsequent phases can reuse parsed state. They are only freed when the top-level workflow ends.
| level 0 (parent) | level 1 (under a reference step) | |
|---|---|---|
| Target | Host device | Component(s) selected via compatibility of the child manifest, using the registered Component Enumerator |
selectedComponents set on step workflow? |
No (host) | Yes, one component at a time |
| If no Component Enumerator registered | n/a | The level-1 reference step is treated as targeting the host (single iteration, no component data) |
If components match compatibility but count == 0 |
n/a | Step is treated as optional and short-circuits with ADUC_Result_Install_Skipped_NoMatchingComponents (or ADUC_Result_Download_Skipped_NoMatchingComponents during Download) |
A child update may not contain reference steps — only one level of indirection is allowed. The service validates this at import time.
The agent's core orchestrator calls the eight ContentHandler functions in the order: IsInstalled → Download → Backup → Install → Apply → (on failure) Restore → Cancel (asynchronous). The Steps Handler implements them as follows.
StepsHandler_Download (lines ~534–710 of steps_handler.cpp):
- If cancel was requested before entry, return
ADUC_Result_Failure_Cancelledimmediately. - Create the sandbox work folder (
ADUC_ERC_STEPS_HANDLER_CREATE_SANDBOX_FAILUREon error). PrepareStepsWorkflowDataObject— ensure all child workflows exist (downloads detached manifests for reference steps).- Determine
selectedComponentsCount(1 for level 0 or when no enumerator; otherwise the count of matched components — 0 means the step is treated as optional and is skipped viaADUC_Result_Download_Skipped_NoMatchingComponents). - For each (component × step):
- Set per-step
selectedComponents(inline steps only). - Pick handler: inline step → step's
handlerproperty; reference step →microsoft/steps:1. LoadUpdateContentHandlerExtension— fail fast on load error.- Call the step handler's
IsInstalled. If it returnsADUC_Result_IsInstalled_Installed, storeADUC_Result_Install_Skipped_UpdateAlreadyInstalledon the step and continue to the next step (the workflow is not aborted). - Otherwise call the step handler's
Download. Any failure aborts the whole Download phase (goto done) and the parent'sresult_detailsis populated from the failing step.
- Set per-step
- On overall success the parent state becomes
ADUCITF_State_DownloadSucceededand result isADUC_Result_Download_Success.
Note: Existing inline-step "already installed" → skip behavior applies per step. The Learn doc's claim that the agent "skips future steps if an update is already installed" is not what the implementation does.
StepsHandler_Install (lines ~768–1165). The Install phase is by far the most behavior-rich:
- Cancel pre-check, sandbox creation,
PrepareStepsWorkflowDataObject, component selection — same as Download. - Outer loop: each selected component. For level-0 or no enumerator, this loop iterates exactly once with no component data set on the step workflow.
- Inner loop: each step (child workflow).
- Set component data (inline steps only).
- Load the step handler.
- Call
IsInstalled; ifInstalled, mark the step withADUC_Result_Install_Skipped_UpdateAlreadyInstalledandgoto instanceDone(skip Backup/Install/Apply, but continue to the next step). - Call
Backup(handler-defined; may be a no-op). Failure ⇒ propagate details andgoto done(abort whole Install phase). - Call
Install. The handler may request reboot/restart via the workflow API:- Immediate reboot/restart requested — propagate to parent (
workflow_request_immediate_reboot/workflow_request_immediate_agent_restart) andgoto doneto skip all remaining components and steps. - Deferred reboot/restart requested — propagate to parent and
breakout of the inner step loop (skip remaining steps for this component but continue with the next component). This is intentional: deferred requests group at the component boundary.
- Immediate reboot/restart requested — propagate to parent (
- If the handler returned
Install_Skipped_UpdateAlreadyInstalledorInstall_Skipped_NoMatchingComponents,goto instanceDone(skip Apply). - On Install failure, call the handler's
Restore(best-effort, result discarded), propagateresult_detailsto parent, andgoto done. - Call
Apply. On failure, callRestore(best-effort) and fall through toinstanceDone. The failing apply's result code stays onresult, so the post-loop checkif (IsAducResultCodeFailure(result.ResultCode)) goto componentDone; ⇒ goto doneaborts the whole phase. (See Concerns below — this control flow is subtle.)
- On overall success, return
ADUC_Result_Install_Successand set stateADUCITF_State_InstallSucceeded.
The parent-level Apply is a no-op (returns ADUC_Result_Apply_Success). The actual per-step Apply was already invoked inside Install. This is by design but is non-obvious; preserve this behavior when implementing custom Update Manifest Handlers.
Sets WORKFLOW_PROPERTY_FIELD_CANCEL_REQUESTED = true on the workflow handle (workflow_request_cancel). Each step handler is responsible for checking this flag during its own work and short-circuiting with ADUC_Result_Failure_Cancelled. The Steps Handler itself only checks the flag:
- At the top of Download / Install / Apply / Backup (returns immediately).
- At the bottom of Install (after all components/steps run — to overwrite a
Install_SuccesswithFailure_Cancelledwhen cancel raced with completion).
A cancel raised mid-Install will not be honored at the parent level until a step handler returns control. Step handlers must poll the flag.
StepsHandler_IsInstalled (lines ~1293–1517) returns ADUC_Result_IsInstalled_Installed only when every (component × step) pair returns Installed. Any single non-installed step ⇒ ADUC_Result_IsInstalled_NotInstalled. Failures inside a step's IsInstalled are converted to NotInstalled (the workflow is processed). A selectedComponentsCount == 0 (no matching components) returns Installed so the optional step is silently met.
Top-level Backup and Restore are no-ops. The Steps Handler invokes per-step Backup/Restore from inside Install as described above.
| Request flag set by step handler | Action by Steps Handler |
|---|---|
workflow_request_immediate_reboot |
Propagate to parent, goto done — abort all remaining components & steps |
workflow_request_immediate_agent_restart |
Propagate to parent, goto done |
workflow_request_reboot (deferred) |
Propagate to parent, break inner step loop; continue with next component |
workflow_request_agent_restart (deferred) |
Propagate to parent, break inner step loop; continue with next component |
The top-level orchestrator inspects these flags after Install returns and performs the actual reboot / restart through the agent's reboot synchronization protocol (see architecture-overview.md → Graceful Reboot Flow).
Steps Handler ⇒ parent workflow result codes:
| Phase result | Meaning |
|---|---|
500 ADUC_Result_Download_Success |
Every step downloaded (or was already installed / had no matching components). |
502/503/504 |
Forwarded from a step handler's Download skip cases. |
600 ADUC_Result_Install_Success |
Every step installed and applied. |
603 ADUC_Result_Install_Skipped_UpdateAlreadyInstalled |
Set on a step when its handler reports IsInstalled_Installed. |
604 ADUC_Result_Install_Skipped_NoMatchingComponents |
Set on a step when 0 components matched. |
700 ADUC_Result_Apply_Success |
Always (parent-level Apply is a no-op). |
800 ADUC_Result_Cancel_Success / 801 ADUC_Result_Cancel_UnableToCancel |
From workflow_request_cancel. |
900 / 901 |
Aggregate IsInstalled result. |
1000 / 1100 |
Backup/Restore — always success at parent level. |
0 ADUC_Result_Failure |
Generic failure; check ExtendedResultCode. Common Steps-Handler ERCs: ADUC_ERC_STEPS_HANDLER_CREATE_SANDBOX_FAILURE, ADUC_ERC_STEPS_HANDLER_INSTALL_FAILURE_MISSING_CHILD_WORKFLOW, ADUC_ERC_STEPS_HANDLER_SET_SELECTED_COMPONENTS_FAILURE, ADUC_ERC_STEPS_HANDLER_INSTALL_UNKNOWN_EXCEPTION_*. |
-1 ADUC_Result_Failure_Cancelled |
Cancel was requested before/during the phase. |
For the full enumeration see device-update-agent-extended-result-codes.md.
Set the environment variable DU_AGENT_ENABLE_STEPS_HANDLER_EXTRA_DEBUG_LOGS to any non-empty value to enable verbose per-step / per-component traces. Useful when reproducing component-selection issues with the Contoso example component enumerator.
| File | Role |
|---|---|
src/extensions/update_manifest_handlers/steps_handler/src/steps_handler.cpp |
All phase logic |
src/extensions/update_manifest_handlers/steps_handler/src/handler_create.cpp |
Exported CreateUpdateContentHandlerExtension factory |
src/extensions/update_manifest_handlers/steps_handler/inc/aduc/steps_handler.hpp |
StepsHandlerImpl class declaration |
src/extensions/update_manifest_handlers/steps_handler/README.md |
Examples + sequence diagrams |
src/extensions/update_manifest_handlers/steps_handler/tests/steps_handler_ut.cpp |
Catch2 unit tests |
- Update Manifest v5 Schema — manifest structure consumed by this handler
- Device Update Agent Extensibility Points — where Steps Handler fits in the extension model
- Multi Component Updating — how reference steps target components
- How To Implement Custom Update Handler — for authoring step handlers that the Steps Handler will load
- Steps Handler README (with sequence diagrams)
- Extended Result Codes