An approval node in AWX is one person clicking yes on a screen that does not say what yes means. Two changes in Ascender make approval nodes usable as a production gate: a node can require several approvers and keep the trail of who voted, and it can show the approver what they are approving. All of it is off by default, so an approval node you already have behaves exactly as it does today.
More than one approver
An approval node carries a Required approvals count, the number of distinct users that must approve before the node moves on. Until that many have, the node stays pending and the workflow waits. A single deny ends it immediately: there is no quorum to reach for a refusal, which is how required reviewers work in GitHub Actions.
Each user gets one vote. A second vote from the same user is refused with a 400, and while the node is pending the approval detail shows how many approvals have been received against how many are required.
Every vote is kept
Each approve and deny is recorded as a row of its own: who voted, which way, when, and an optional comment.
/api/v2/workflow_approvals/N/votes/ for the votes on one approval.
/api/v2/workflow_approval_votes/ for all of them, filterable by user, vote and date like any other list.
Comments are API only for now, a comment in the body of the approve or deny call. The buttons in the UI are single click, so a vote cast from the screen carries none.
The trail outlives what it describes. Votes cannot be edited through the API, and when an approval or a user is deleted the links are severed rather than the votes deleted with them, with the approval name, the workflow job id and name and the username kept on the vote itself. Jobs get cleaned up eventually, and who approved the production change should not go with them. You can read the votes on any approval you can already see, and the orphaned ones, whose approval has been purged, are left to superusers and system auditors.
Something to approve
The other half is a context template on the approval node: Jinja2, rendered when the workflow reaches the node, against the artifacts the upstream jobs published with set_stats. The result is stored on the approval and shown both in the approval detail and in the notification that says the node needs review.
That is the dry run pattern with no new plumbing. One node runs the playbook in check mode and publishes what it found, the approval node renders it, the node after it applies for real. The approver reads the diff on the approval screen instead of opening the previous job and scrolling through its output.
Rendering someone else's Jinja2 on the control plane needs care, so it happens in a disposable forked process with CPU and memory limits and a hard timeout, in the sandbox the notification templates use, with the output capped. A hostile or broken template is logged and the approval is created without a context, never a stuck workflow.
The automatic ascender_stats_* artifacts are ordinary artifacts, so they are in scope for the template too, which means "these hosts changed" needs nothing in the playbook.
When the timeout expires
An approval with a timeout used to have one ending, failure. It now takes an On timeout choice: deny, which is what it did before and remains the default, or approve, which marks the node successful and lets the workflow carry on. Either way the node is flagged as timed out, with an explanation saying the resolution was automatic.
Where the fields are
All three sit in the approval step of the node form in the workflow visualizer, and on workflow_approval_templates in the API as required_approvals, on_timeout and context_template. The defaults are one approval, deny on timeout and an empty context, which is an approval node exactly as it behaves now.
Both changes are @fernandorocagonzalez's, in #578 and #579.
Two things are deliberately not there yet, and both are worth an opinion first: a comment box on the approve and deny buttons, and a quorum that names who must approve rather than counting anyone who can.