Repository navigation
Logging and Reporting
StartSet writes three things: a Serilog stream (console and the Windows event log), a per-run session directory on disk, and a set of aggregated reports for monitoring tools to read.
| Sink | When | Minimum level |
|---|---|---|
| Console | CLI only (not the service) | the configured level |
Windows event log — log Application, source StartSet
|
service mode, and CLI when the configured level is at or below Warning | Warning |
There is no Serilog file sink. File logging is entirely the session logger's job.
The level comes from Config.yaml: log_level if set, otherwise Debug when debug or verbose
is true, otherwise Information. The --verbose / --debug command-line options do not change it.
Read the event log for a quick view of failures:
Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='StartSet'} -MaxEvents 50If the source has never been registered, register it once from an elevated shell:
New-EventLog -LogName Application -Source StartSetEvery run — a service boot pass, a logon pass, a trigger pass, a CLI invocation — opens a session.
C:\ProgramData\ManagedState\logs\YYYY-MM-DD\HHMM\
├── startset.log human-readable
├── session.json session metadata and summary
└── events.jsonl structured event stream, one JSON object per line
The session id is YYYY-MM-DD-HHMM. Two sessions in the same minute collide, and the second gets
a _2 suffix on the directory and the id, up to _9.
The run type is recorded in session.json and is one of boot, login, trigger, or — from the
CLI — the first argument you typed (so a managedstatekeeper list run has run type list).
startset.log lines look like this:
[2026-03-01 08:14:22] INFO Found 3 scripts in C:\ProgramData\ManagedState\login-every
[2026-03-01 08:14:24] INFO [Map-DepartmentDrives] stdout | Mapped R: to \\fileserver\share
[2026-03-01 08:14:24] WARN Deferred Set-Wallpaper.ps1: could not reach the console user's session (no active console session). The script did NOT run.
Payload stdout and stderr are folded into this log when log_script_output is true, prefixed
[<script name>] stdout | or [<script name>] stderr |, capped at 500 lines per stream per
payload with a ... output truncated after 500 lines marker. There are no per-script sidecar
files; a payload runs as a child of the StartSet process so its output is already in hand, and
splitting the record in two only made it harder to read.
{
"session_id": "2026-03-01-0814",
"start_time": "2026-03-01T08:14:20.1234567-08:00",
"end_time": "2026-03-01T08:14:31.7654321-08:00",
"run_type": "login",
"status": "completed",
"duration_seconds": 11,
"summary": {
"total_scripts": 3,
"succeeded": 2,
"failed": 0,
"skipped": 1,
"scripts_handled": ["Map-DepartmentDrives.ps1", "Set-Wallpaper.ps1", "Configure-Taskbar.ps1"]
},
"environment": {
"hostname": "WORKSTATION01",
"user": "SYSTEM",
"os_version": "Microsoft Windows NT 10.0.26200.0",
"architecture": "x64",
"process_id": 4812,
"log_version": "1.0"
}
}status is running while the session is open, then completed, completed_with_errors or
failed. Note the summary counts do not carry a deferred column — deferred payloads are
visible in events.jsonl and items.json, and in the log warnings.
One JSON object per line. Script executions look like this:
{"event_id":"2026-03-01-0814-638...","session_id":"2026-03-01-0814","timestamp":"2026-03-01T08:14:24.12","level":"INFO","event_type":"script_execution","script_name":"Map-DepartmentDrives.ps1","action":"LoginEvery","status":"completed","message":"Exit code 0","duration_ms":1834}| Field | Values |
|---|---|
event_type |
script_execution |
action |
the payload type |
status |
completed, failed, skipped, deferred
|
level |
ERROR when failed, INFO when completed, otherwise DEBUG
|
duration_ms, error
|
present when applicable |
C:\ProgramData\ManagedState\reports\ is regenerated at the end of every session. It is the
directory a monitoring or inventory agent should read.
| File | Contents |
|---|---|
sessions.json |
The last 100 sessions' session.json documents, newest first |
events.json |
Every event from the last 10 session directories that is newer than 48 hours |
items.json |
One record per payload this session considered |
run.log |
A copy of the current session's log, truncated and rewritten each session |
run.log is the "what happened most recently" file; it is deleted and recreated at the start of
every session, so do not treat it as history.
One record per payload, in the same shape sibling tooling uses for managed items so a single
reader serves both. The type field is startset.
[
{
"id": "login-every/map-departmentdrives.ps1",
"item_name": "Map-DepartmentDrives.ps1",
"display_name": "Map-DepartmentDrives",
"item_type": "ps1",
"current_status": "Installed",
"latest_version": "",
"last_seen_in_session": "2026-03-01-0814",
"last_attempt_time": "2026-03-01T16:14:22.0000000Z",
"last_attempt_status": "Installed",
"last_update": "2026-03-01T16:14:31.0000000Z",
"failure_count": 0,
"warning_count": 0,
"type": "startset",
"last_error": "",
"action_performed": "execute"
}
]Status mapping:
| Execution outcome | current_status |
|---|---|
| Success | Installed |
| Skipped because the run-once ledger says it already ran | Installed |
| Skipped for any other reason | Pending |
| Deferred — user session unreachable | Pending |
| Failed, timed out, unsupported type | Error |
action_performed is install for .msi/.msix payloads and execute for everything else, and
is absent for skipped and deferred payloads. last_seen_in_session is stamped only when the
run actually acted on the payload, so filtering on a non-empty value gives you what this run did.
failure_count is derived by counting failed script_execution events across the last 50
session directories within the last 7 days, folded together with whether the payload failed in
this run. last_error and last_warning are the first non-blank line of the error or of stderr,
truncated to 500 characters — a payload that succeeded but wrote to stderr gets a last_warning,
which is the nearest thing a script has to a warning signal.
A session that considered no payloads leaves the previous items.json in place rather than
writing an empty one.
Retention runs asynchronously at the start of every session and is 30 days.
| Target | Rule |
|---|---|
logs\YYYY-MM-DD\ day directories |
Deleted whole when the date is older than the cutoff |
Loose files directly in logs\
|
Deleted when last written before the cutoff |
Files in logs\installs\
|
Deleted when last written before the cutoff |
The loose-file sweep is deliberately not recursive — session directories are aged as a unit by
date, and picking old files out of one individually would leave half a session's log set behind.
It is also not filtered by filename: anything sitting loose in the log directory is treated as a
log and aged. If a payload of yours writes files into C:\ProgramData\ManagedState\logs, they
will be deleted after 30 days. Write payload output somewhere else if you need it kept.
reports\ is not aged — its files are rewritten in place each session.
Latest session directory:
Get-ChildItem C:\ProgramData\ManagedState\logs -Directory |
Sort-Object Name -Descending | Select-Object -First 1 |
Get-ChildItem -Directory | Sort-Object Name -Descending | Select-Object -First 1Most recent run's log:
Get-Content C:\ProgramData\ManagedState\reports\run.logEverything that did not come out clean:
Get-Content C:\ProgramData\ManagedState\reports\items.json |
ConvertFrom-Json | Where-Object current_status -ne 'Installed'Deferred payloads across recent sessions:
Get-Content C:\ProgramData\ManagedState\reports\events.json |
ConvertFrom-Json | Where-Object status -eq 'deferred'StartSet — MIT licensed — windowsadmins/startset — a Windows port of macadmins/outset.
StartSet
How it works
Migrating
Operating
Contributing