Scheduled changes
What it does
Pushes the apply step on a change request to a near-future time. The change still goes through the standard draft → dry-run gate, but instead of applying immediately on Approve, Policytab parks it until the schedule fires and a cron-driven Edge Function picks it up.
The schedule itself is just a scheduled_for timestamp on the change_request row. The Edge Function changes-scheduled-apply runs every five minutes and applies every dry_run_complete row whose scheduled_for <= now(). The scheduled time must be within the 30-minute dry-run validity window; re-run the dry-run closer to a longer maintenance window.
When to use it
- Short maintenance windows. Customer wants the change to land in a few minutes, not immediately.
- Coordinated cutovers. Several tenants need the same change at the same wall-clock instant.
- Coordinated final clicks. Dry-run immediately before a change window, then schedule the actual write for the first cron tick in that window.
For an immediate change, skip the schedule - leaving scheduled_for empty preserves the normal "Approve and apply now" behaviour.
Step-by-step
- Tenant detail → Changes → Propose change, same as the immediate flow.
- Build the payload and click Dry-run. Review the warnings.
- Tick Schedule for later. A datetime picker appears, defaulting to roughly "now + 15 minutes" in your local zone.
- Pick the apply time. The picker shows your local time; the value stored on the row is UTC. The selected time must be within 30 minutes of the current dry-run.
- Click Schedule. The row enters
dry_run_completewithscheduled_forpopulated. The dashboard's Scheduled changes card shows it counting down. - The cron runs at
*/5 * * * *. At the first tick afterscheduled_for, the Edge Function flips the row toapplying, runs the standard apply (pre-snapshot → Graph PATCH → post-snapshot), and writesappliedon success.
Cancelling a scheduled change
Before the cron fires:
- Open the change.
- Click Cancel schedule. The row stays
dry_run_complete,scheduled_foris cleared, and an audit row recordschange_request.schedule_cancelledwith your user id.
After the cron has started applying, you cannot cancel - the apply is one-shot. Use Rollback on the applied change instead, exactly as you would for any other applied change.
Combining with the approval workflow
If require_approval is on (see Approval workflow), the order is:
- Requester runs dry-run →
awaiting_approval. - Approver reviews + approves →
dry_run_complete. - Admin schedules within the remaining dry-run window.
- Cron fires at the scheduled time → applies.
If approval doesn't happen before the schedule, the cron does not apply the change - awaiting_approval is excluded from the scheduler's WHERE clause. The change waits indefinitely until an approver acts.
What the cron actually does
The query is intentionally narrow:
select id
from tenant_<uuid>.change_request
where status = 'dry_run_complete'
and scheduled_for is not null
and scheduled_for <= now();
The partial index change_request_scheduled_idx covers this shape so even workspaces with thousands of historical changes scan only scheduled rows per tick.
Troubleshooting
- "Schedule" button missing. The dry-run produced a critical warning - same gate as an immediate apply. Resolve the warning (or open a change that addresses it) before scheduling.
scheduled_foris in the past on a fresh row. The UI rejects past times; if you see this on a row, an admin probably edited the datetime in DB or via the API. The cron will pick it up immediately on its next tick - cancel if that's not what you want.- My scheduled change never applied. Check three things in order:
- Is the row still in
dry_run_complete? (awaiting_approvalparks waiting for approval;cancelledmeans someone cancelled.) - Is the dry-run still valid? Scheduling beyond the 30-minute dry-run TTL is rejected; if it expired anyway, re-run dry-run and schedule again.
- Is the cron job actually scheduled?
select * from cron.job where jobname like '%scheduled%'in the SQL editor. Operators enable it via the Vault-backed cron migrations.
- Is the row still in
- Apply fails after the cron picks it up. Standard apply error handling - the row lands in
failedwitherror_messagepopulated. Same recovery steps as a manually applied change that failed; see Troubleshooting. - DST handling.
scheduled_foristimestamptz. The UI converts to/from your browser zone, so a "9 PM Friday" pick is stable across DST shifts at the storage layer.
Operator note
The cron job is registered by the Vault-backed cron migrations. Operators must set edge_functions_base_url and edge_functions_service_key in Vault for the kick function to call the Edge Function.