Skip to content

feat(scheduler): surface failed and at-risk scheduled tasks - #202

Open
renada-jacob wants to merge 7 commits into
CyberDrain:devfrom
Renada-Solutions:feat/scheduler-failure-visibility
Open

feat(scheduler): surface failed and at-risk scheduled tasks#202
renada-jacob wants to merge 7 commits into
CyberDrain:devfrom
Renada-Solutions:feat/scheduler-failure-visibility

Conversation

@renada-jacob

@renada-jacob renada-jacob commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

I've run into a problem many times over where for example: if you schedule a new user creation using available licences based on CIPP's license count check, and by the time the scheduled task runs the licence has been used or otherwise is no longer available, the user is created unlicensed the task shows Completed (successfully), but nothing tells you that creating that user without a license comes with its own problems.. the user wasnt added to the mail-enabled groups you wanted because it doesnt have a mailbox, and oh yeah the user doesnt have a mailbox - you wanted that right?... and the worst bit is nothing anywhere tells you. Set-CIPPUserLicense returns the Graph error as a string rather than throwing, so the failure only exists as text inside Results on a row that might not get looked at. Outright failures aren't much better. The executor sets $State = 'Failed' when a command throws, but only ever read it for orchestrator-based commands, so every other failing task was written as Completed and logged as "Successfully executed task".

This PR aims to fix this problem and does the following:

Failed tasks now report as failed. $State is honoured in the terminal writes, with recurring tasks going to Failed - Planned so they still reschedule. Errors logged during a run are collected against the task through the same AsyncLocal context that already stamps ScheduledTaskId onto log rows, so a task that finishes while a step inside it failed gets HasErrors and an ErrorSummary naming what went wrong. That's what catches the licence case. Multi-tenant tasks flag partial failures too; previously 3 failed tenants out of 50 aggregated to a clean Completed.

Planned tasks that would fail get flagged before they run. A six-hourly preflight check walks planned licence-dependent tasks and compares what they need against what the tenant has left. Tasks that would fail today are marked at risk with a readable reason, "no licences available for Microsoft 365 E5 Developer" rather than a bare SKU id (resolved through Convert-SKUname), and the flag clears on its own when the licence comes back, when the task is edited, or when the task runs. Sherweb-backed creations are skipped since they buy the licence at run time. Licence availability is the first check, but the mechanism doesn't care what the predicate is: a username already taken or a deleted target mailbox can append to the same problems list later without touching anything downstream. The timer runs at :07 past its hours so its writes never race the orchestrator's ETag claim on the */15 boundary.

Two new scheduler tabs, using the same route-based tab pattern as the applications and alert-configuration groups. Failed Queue is everything needing attention, including completed-with-errors. Pending with Issues is the flagged planned tasks, with a Re-check Now button for after you've fixed the underlying problem. The endpoint runs the check synchronously so the refreshed table shows the updated flags, and it's exposed at Scheduler.ReadWrite because the only existing way to run it on demand is ExecCippFunction, which needs SuperAdmin.

image image

Acknowledge Errors on failed rows, for failures remediated out of band (previously the only way out of the view was deleting the task). It records who and when, and the row stops demanding attention but keeps HasErrors and the summary: the task did fail, and the history should say so. Any later run that fails clears the acknowledgement, so a recurring problem resurfaces rather than staying dismissed.

image

Followed by the confirmation modal after selection:

image

Both sides can notify: two new log types in notification settings, "Scheduled task failures" and "Tasks pending with issues", routing through the existing pipeline to email, webhook and PSA. They're separate on purpose. One is a thing that happened, the other is a forecast, and you may well want one without the other.

image

Nothing existing changes shape. The new columns are additive and default to false/empty on existing rows, the new ListScheduledItems parameters are all optional, and the parameterless response is unchanged. Filter input goes through ConvertTo-CIPPODataFilterValue. The acknowledged exclusion is applied in memory rather than in the OData filter on purpose: Acknowledged ne true drops rows that don't carry the property at all, which is every task written before this, so it would have silently emptied the view.

Also unwraps hashtable throws in the executor's catch. New-CIPPUserTask reports a failed creation with throw @{'Results' = ...}, and a thrown hashtable's Exception.Message is the literal type name, so the stored result read "Task Failed: System.Collections.Hashtable" instead of what actually went wrong.

Worth flagging for review: the multi-tenant aggregation decides success by string-matching results for "Failed" and "Error". That's pre-existing, but HasErrors now makes its false positives more visible, so a success message containing the word "Error" will flag the task. Fixing it properly means changing how per-tenant results are shaped, which felt like its own PR.

Tested by scheduling real tasks through the API and letting the unmodified timer cadence run them over two days against a sandbox tenant:

  • a creation with no licence available: user created, task Completed with HasErrors and the exact Graph error in the summary, sat in the Failed Queue
  • the same username scheduled again: threw on the duplicate UPN, landed as Failed with a readable summary
  • a recurring licence assignment against a deleted user: failed and rescheduled five times over 20 hours, flagged throughout, never stuck
  • a creation scheduled a day out with no licences left: flagged at risk by the overnight preflight with the friendly SKU name in the reason
  • freed one licence with two flagged tasks waiting: the first to run took it and left the at-risk view clean, the preflight seven minutes later correctly kept the second flagged, and when that one ran it failed and moved to the Failed Queue with its flag cleared
  • clean control with no licence involved: Completed, unflagged, in neither view
  • acknowledged a failed row: dropped from the queue, history intact, came back when it failed again

PS - this is rather large commit of mine I have spent a decent amount of time working on, but with that being said, I would appreciate insight that anyone has to things that should be adjusted/updated/removed etc.

A scheduled task whose command threw was written to storage as Completed.
$State is set to 'Failed' in the executor's inner catch, but it was only
ever read for orchestrator-based commands, so every other failing task
fell through to the success path and the run was logged as "Successfully
executed task".

Partial failures were worse. A scheduled user creation whose licence
assignment failed never threw at all, because Set-CIPPUserLicense returns
the Graph error as a string rather than raising it. The user was created
unlicensed and the task reported success, so nothing surfaced it.

- Honour $State when writing terminal task state. Recurring tasks still
  reschedule, via 'Failed - Planned'.
- Collect Error and Critical log entries against the running task using
  the existing scheduled-task AsyncLocal context, and record them as
  HasErrors and ErrorSummary. This catches steps that fail without
  throwing, which is the licence case.
- Flag multi-tenant tasks with partial failures. They previously reported
  Failed only when every tenant failed, so 3 failures out of 50 looked
  like a clean run.
- Add Start-CIPPTaskPreflightCheck, a timer that flags planned tasks whose
  licence is no longer available before their scheduled time arrives.
- Add State, HasErrors, AtRisk and NeedsAttention filters to
  ListScheduledItems, and Failed Queue and Pending with Issues tabs to the
  scheduler page.
- Add Scheduler_UserTasks to the notification log types so task failures
  can raise an alert instead of waiting to be noticed.

HasErrors, ErrorSummary, AtRisk and AtRiskReason are new properties and
default to false/empty on existing rows, so the endpoint's output is
unchanged for callers that do not ask for them.
The preflight check marks a Planned task AtRisk when the licence it needs
is no longer available. That flag is a prediction about a run that has not
happened yet - but nothing cleared it afterwards, and the preflight only
re-examines tasks still in the Planned state. So a flagged task that was
run anyway (RunNow, or on schedule) kept AtRisk forever and sat in the
Pending with Issues view as a Completed task.

Every write that takes a task out of the Planned state now clears AtRisk
and AtRiskReason: the three early failure paths (missing command, module
not allow-listed, blocked command), the executor's outer catch, the failed
orchestrator dispatch, and both terminal writes.

Two paths deliberately keep the flag: the DeltaQuery early-return puts the
task back to Planned without running it, so the prediction still stands,
and the multi-tenant post-execution aggregate is untouched because
AllTenants and tenant-group tasks are never preflighted in the first
place.
New-CIPPUserTask reports a failed creation by throwing a hashtable that
carries its partial results (throw @{'Results' = ...}). A thrown
hashtable's Exception.Message is the literal type name, so the stored
task result read "Task Failed: System.Collections.Hashtable" instead of
the actual error - surfaced by the failed-task views, which make the
Results column far more visible than it used to be.

Unwrap the thrown object's Results when it is a dictionary, and fall back
to Exception.Message for ordinary throws.
…le at-risk reasons

Closes the operational gaps in the failed and at-risk task views.

Acknowledge: a task that completed with errors could only leave the
needs-attention view by being deleted, even after the underlying problem
was fixed out of band. The new Acknowledge Errors action records who
acknowledged and when, and the needs-attention filter skips acknowledged
rows. HasErrors and ErrorSummary stay on the row on purpose - the task
did fail and the history should say so - and every run that records a
failure resets the acknowledgement, so a recurring problem resurfaces.
The acknowledged filter is applied in memory: an OData clause like
"Acknowledged ne true" drops rows that lack the property entirely, which
is every task written before this existed.

Re-check Now: the preflight runs six-hourly, so after remediating a
licence shortage the at-risk view could lag by hours. The new endpoint
runs the check on demand at CIPP.Scheduler.ReadWrite - ExecCippFunction
can already do this but requires SuperAdmin - and runs it synchronously
so the caller's refresh shows the updated flags.

At-risk notifications get their own log type (Scheduler_Preflight,
"Tasks pending with issues") instead of sharing Scheduler_UserTasks:
at-risk is a prediction about a task that has not run, and admins may
want those separately from actual failures.

At-risk reasons now resolve SKUs through Convert-SKUname, so the view
says "no licences available for Microsoft 365 E5 Developer" rather than
DEVELOPERPACK_E5, falling back to the part number for unmapped SKUs.

The at-risk mechanism itself is check-agnostic: licence availability is
the first preflight check, and further predicates (username already
taken, target mailbox or group deleted, tenant unreachable) can append
to the same problems list without touching the views or notifications.
…heduler-failure-visibility

# Conflicts:
#	backend/Config/CIPPTimers.json
@renada-jacob
renada-jacob marked this pull request as ready for review August 6, 2026 14:54
Adds pages for the Failed Queue and Pending with Issues tabs, covering
what lands in each view, the acknowledge workflow, the six-hourly
availability check and the Re-check Now button. Updates the scheduler
overview for the new tabs, columns and action, and lists the two new
notification log types on the notifications page.
@renada-jacob

Copy link
Copy Markdown
Contributor Author

Docs added in new commit: new pages for the Failed Queue and Pending with Issues tabs, covering what lands in each view, the acknowledge workflow, and the six-hourly availability check with the Re-check Now button. The scheduler overview picks up the new tabs, columns and action, and the notifications page lists the two new log types. Both pages are registered in SUMMARY.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant