Scheduling travel access in Pia: schedule_for_later doesn't wait
Travelers need to be in their company's travel-allowed group while they're away. Someone has to add them on the day they leave and remove them the night they get back, and that someone used to be an engineer with a calendar reminder. The first version of this automation was hardcoded to one customer. This one works for any client tenant, and the hard part turned out to be the scheduling step.
The date-handling bug is its own post: Time zone bugs in scheduled actions. This one covers the rest.
What the engineer sees
- One form: pick the traveling user and the travel group from dropdowns built live from the client's tenant, then set the leave and return dates.
- A chat message saying exactly what will happen: scheduled for those dates, or "leave date is today, adding them now."
- An internal note on the ticket at setup, another when the user is added, and a last one when they're removed. Each note is stamped with the time it actually ran, because that can be days after setup.
- A reminder to put the ticket on hold and verify on those days.
Only list groups Graph can actually change
The user list is every enabled member account. Guests and disabled leavers are left out.
The group list is anything with "travel" in the name, as long as Graph can add members to it. That means cloud security groups (security-enabled, not mail-enabled) and Microsoft 365 groups. Dynamic groups, groups synced from on-premises AD, and mail-enabled security groups are skipped, because the group-add step would fail on them. Better to never offer a group than to fail on it three days later.
If the tenant has only one travel group, it comes pre-selected. Both lists page through Graph 999 at a time.
schedule_for_later doesn't wait. It forks.
This one took me the longest. I assumed schedule_for_later paused the run until the date. It doesn't. The live run carries straight on past it, and a separate copy of the run resumes later, on the scheduled date, with running_schedule set to true.
So without a guard, the step right after the schedule, adding the user to the group, runs immediately on the live run, days early. Then the scheduled copy runs it again on the right day.
The fix is a small check after every schedule step that decides whether this copy should keep going:
$skipped = ([string]$skippedSchedule).Trim('"') -eq 'true'
$running = ([string]$runningSchedule).Trim('"') -eq 'true'
$shouldProceed = $skipped -or $running
$stopCondition = if ($shouldProceed) { "False" } else { "True" }
Keep going if scheduling was skipped, because the date was today and there was nothing to wait for. Keep going if this is the scheduled copy, because the date has arrived. Otherwise this is the live run that just set up the wait, and it stops. A stop task right after the check does the stopping.
The same check guards the removal. Chat messages after the group-add only go out on the same-day path, since nobody is watching the scheduled copy when it runs.
One more detail: the skip on the schedule step is the literal string "true" or "false", built in PowerShell. That's the only thing Pia's skip condition accepts.
Checks before anything changes
- Add at 12:00 AM on the leave date and remove at 11:59 PM on the return date, in the market's time zone, converted to UTC for the scheduler.
- Leave and return on the same day, or out of order: stop and say so. If the trip really is just today, set the return date to tomorrow.
- Return date already past: stop. There's nothing to schedule.
- Leave date is today: add the user now and schedule only the removal.
All of that runs before the setup note is posted, so a bad date never leaves half a plan on the ticket.
The takeaway
In Pia, a scheduled step is a second copy of your run, not a pause. Guard everything after it, or it happens twice: once now and once on time.