Skip to content

Feature flag rollout approval: gate 5%, 25%, and 100% exposure

A copy-paste feature flag rollout approval workflow that gates each exposure stage with evidence, guardrails, sequential decisions, and rollback.

Software dashboard with multiple status panels

The code is deployed. The flag is off. The question is not "can we ship?" — it's "should 25% of production accounts see this behavior?" An exposure gate puts a human decision between one rollout stage and the next, with the evidence and the rollback plan right next to the buttons.

Architecture Flow
No Yes Approve Reject / timeout Deploy flag off Collect guardrails Metrics healthy? Hold or rollback Actionbox approval Increase exposure Hold current percentage

The pattern: approve exposure, not deployment

A deployment gate answers "is the artifact good enough for an environment?". An exposure gate answers a different question: "is the evidence good enough to widen the audience?" Keep them separate:

StageWhat changesGate
DeployArtifact lands on servers, flag offCI checks, no human needed
InternalFlag on for the teamNo gate
Beta5% of low-risk accountsApprove with evidence
Ramp25% of productionApprove with guardrails
Launch100%Approve + rollback check

Step 1: the rollout job creates the approval

bash
#!/usr/bin/env bash
# promote-flag.sh — ask before widening exposure
set -euo pipefail

FLAG_KEY="${1:?flag key}"
NEXT_PERCENT="${2:?next rollout percent}"

CURRENT=$(curl -s -H "Authorization: Bearer $FLAGS_API_TOKEN" \
  "$FLAGS_API_URL/flags/$FLAG_KEY" | jq -r .rollout.percent)

if [ "$NEXT_PERCENT" -le "$CURRENT" ]; then
  echo "Rollout is not increasing ($CURRENT% -> $NEXT_PERCENT%)"
  exit 0
fi

actionbox ask \
  "Expand ${FLAG_KEY} from ${CURRENT}% to ${NEXT_PERCENT}%?" \
  --option approve="Approve expansion" \
  --option hold="Hold at ${CURRENT}%" \
  --context-json "[{\"type\":\"key_value\",\"items\":{\"flag\":\"${FLAG_KEY}\",\"from\":\"${CURRENT}%\",\"to\":\"${NEXT_PERCENT}%\",\"guardrails\":\"error_rate < 0.5% · p95 < 800ms\"}}]" \
  --callback-url https://ops.acme.com/actionbox/flag-callback

The callback does the actual flag update only when the decision resolves to approve — the script never touches the flag directly.

Step 2: attach the evidence to the approval

An approval without evidence is a rubber stamp. Include the numbers that justify the next stage:

bash
actionbox ask \
  "Expand checkout_v2 to 25% of accounts?" \
  --option approve="Approve expansion" \
  --option hold="Hold at 5%" \
  --context-json "[{\"type\":\"key_value\",\"items\":{\"flag\":\"checkout_v2\",\"from\":\"5%\",\"to\":\"25%\",\"error_rate_24h\":\"0.12%\",\"checkout_completion\":\"+1.4% vs baseline\",\"p95_latency\":\"742ms\",\"support_tickets\":\"0 flag-related\",\"rollback\":\"set flag to baseline for affected segment\"}}]" \
  --callback-url https://ops.acme.com/actionbox/flag-callback

The approver sees: what's changing, the guardrail signals, and the exact rollback action — in the dashboard, before tapping approve.

Step 3: fail-closed on timeout

Nobody answers in time? The rollout should not happen. Make the default explicit:

bash
actionbox ask \
  "Expand ${FLAG_KEY} to ${NEXT_PERCENT}%?" \
  --option approve="Approve expansion" \
  --option hold="Hold at ${CURRENT}%" \
  --expires "$(date -u -d '+4 hours' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u -v+4H '+%Y-%m-%dT%H:%M:%SZ')" \
  --on-expire-json '{"type":"resolve","response":{"type":"single_choice","value":"hold"},"reason":"no response within rollout SLA"}' \
  --callback-url https://ops.acme.com/actionbox/flag-callback

The audit trail records: the request, the decision (or the non-decision), and the reason — so the Friday 5pm "why did the flag stay at 5%?" question answers itself.

Full example: staged rollout with a cleanup reminder

bash
#!/usr/bin/env bash
# staged-rollout.sh — 5% -> 25% -> 100%, each stage gated
set -euo pipefail

FLAG_KEY="billing_checkout_v2"
STAGES=(5 25 100)
ROLLBACK="set ${FLAG_KEY} to baseline for affected segment"

for NEXT in "${STAGES[@]}"; do
  CURRENT=$(curl -s -H "Authorization: Bearer $FLAGS_API_TOKEN" \
    "$FLAGS_API_URL/flags/$FLAG_KEY" | jq -r .rollout.percent)
  if [ "$NEXT" -le "${CURRENT:-0}" ]; then
    continue
  fi

  actionbox ask \
    "Expand ${FLAG_KEY} to ${NEXT}%?" \
    --option approve="Approve expansion to ${NEXT}%" \
    --option hold="Hold at ${CURRENT}%" \
    --context-json "[{\"type\":\"key_value\",\"items\":{\"flag\":\"${FLAG_KEY}\",\"stage\":\"${NEXT}%\",\"rollback\":\"${ROLLBACK}\"}}]" \
    --expires "$(date -u -d '+4 hours' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date -u -v+4H '+%Y-%m-%dT%H:%M:%SZ')" \
    --on-expire-json "{\"type\":\"resolve\",\"response\":{\"type\":\"single_choice\",\"value\":\"hold\"},\"reason\":\"stage timeout\"}" \
    --callback-url https://ops.acme.com/actionbox/flag-callback
done

Why this beats "just merge the PR"

  • Approval is about exposure, not code — the artifact shipped days ago; the decision is about the audience
  • Guardrails travel with the request — error rate, latency, and tickets are shown to the approver, not buried in a dashboard
  • Rollback is one tap away in context — the approver knows the exact revert before approving
  • Same pattern for every flag — LaunchDarkly, Unleash, PostHog, or your own flag service: the gate is just an API call

Sources & Research

Try it

  1. Install the CLI on your release host
  2. Create a Source → ACTIONBOX_TOKEN
  3. Replace your "change the percentage" script with the gated version

Create a free Source · CLI + callback reference · Cron jobs that ask before acting

About the author

Suson Sapkota

Suson founded ActionBox and works in software and data engineering. He writes about approval workflows, background jobs, and how to verify what happened after a human decision.

Turn the next risky operation into a reviewable decision.

Create a free Source, run the example from this guide, and keep the decision and execution outcome connected.