Introduction
Field Notes has approved two workshop room changes after attendance grew. Several session titles and room names repeat, so a safe plan needs exact identities, approval scope and sufficient target capacity.
You will preview a small working change, then build a reusable PowerShell plan validator. This lab assumes PowerShell for Beginners objects, JSON, filtering, parameters and error handling; no Bash scripting is required. Tools and original synthetic event data are prepared in a separate independent workspace. The duration is an author estimate including reading and ordinary errors.
Build a Complete ID-Based Plan
In this step, you will preview one prepared change, then build a reusable validator that saves the complete business plan without changing the schedule.
Enter PowerShell and preview one request
Open Terminal. It initially runs a Linux shell. The following launcher sets UTF-8 only for this PowerShell session; it does not change your global shell. env supplies those environment values to the program. -NoLogo hides the banner and -NoProfile skips personal startup scripts:
env LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 pwsh -NoLogo -NoProfile
Wait for the prompt beginning with PS. All remaining commands run inside PowerShell. Set-Location changes its working directory:
Set-Location /home/labex/project/room-changes
The prepared preview-one.ps1 handles only the single supplied rehearsal request. It supports PowerShell's native -WhatIf: describe an action instead of performing it. This is a small working example, not your reusable multi-request validator. ./ identifies a script in the current directory:
./preview-one.ps1 -WhatIf
Expect a native message containing Move s-a: r-old -> r-equal and target ./rehearsal-only.json. The IDs identify the session and its old and proposed room. It has not moved every session with that title. Read the actual source JSON with Get-Content -Raw, which returns the entire file as one string:
Get-Content ./event.json -Raw
The s-a record still has RoomId equal to r-old. Test-Path checks existence without creating anything:
Test-Path ./rehearsal-only.json
False
If you accidentally omitted -WhatIf, the rehearsal can create that separate file, but it does not overwrite the input. Remove-Item -LiteralPath ./rehearsal-only.json removes only that rehearsal output; use it only if this file exists, then repeat the preview correctly. -LiteralPath treats a path literally rather than expanding wildcard characters. Do not remove the original event or requests to recover from a mistake.
Read the relationship before building the tool
The venue has already checked that target rooms are free at these times. You are checking approved scope and capacity, not solving calendar conflicts. Read the requested mappings and use the prepared read-only consumer to view the actual original schedule:
Get-Content ./requests.json -Raw
./Show-RoomSchedule.ps1 -Path ./event.json
The requests move s-a to r-equal and s-c to r-large. s-a and s-b share the title Shared maps; only s-a is approved for this request. Two rooms are also called Studio. Join using SessionId and RoomId, never the displayed names. The first requested session has 24 attendees and its target capacity is exactly 24: equality is allowed.
event.json, requests.json, small-request.json, bad-request.json and the unrelated notes.txt are originals to preserve, including their bytes, file mode, owner and modification time (mtime). Do not edit the prepared tools. You may create your own scripts and the new result files named in this lab. The course paths and their directory chains are real directories. An output must be new, distinct from the inputs, and have an already-existing real parent; an existing file, directory or link, even a dangling link, must be refused untouched. Work without concurrent writers.
The supplied JSON is well formed UTF-8: an event object with Rooms, Sessions and ApprovedSessionIds, and a nonempty request array containing unique exact session IDs and target room IDs. IDs are case-sensitive strings. Counts and capacities are nonnegative whole-number values. Unknown metadata may contain nested objects, ordered arrays, strings, Boolean values, null and bounded finite JSON numbers, within 20 nesting levels. Exact duplicate object keys, malformed schemas and arbitrary hostile input are outside this course's scope.
Validate every request before saving any plan
You will create Build-RoomPlan.ps1 with three named parameters: EventPath, PlanPath and OutputPath. It must work with other supplied events and IDs, using those arguments and installed commands without neighboring helper files. Its result is a JSON array in request order. Each row has exactly SessionId, OldRoomId, NewRoomId, Attendees and Capacity. This is a business plan describing current object relationships, not a marker claiming that work is complete. It does not change the schedule. The next lab will write a complete updated event instead.
Review these additions before saving the script:
[CmdletBinding()]makes this a parameterized advanced script;[Parameter(Mandatory)]requires an argument, and[string]declares text.try,catch,throwandexit 1follow the prerequisite error-handling lab.$ErrorActionPreference = 'Stop'makes command errors terminate the current operation. ThecatchusesWrite-Error -ErrorAction Continueto display the explanation and then reach the explicitexit 1.- PowerShell's
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(...)resolves the output relative to the current PowerShell directory.Split-Path -LiteralPathreturns its parent.Get-Item -Forcealso discovers hidden objects and dangling links. Here-ErrorAction SilentlyContinueallows the expected absence of a new output.Test-Path -PathType Containerrequires its parent directory. ConvertFrom-Json -AsHashtablekeeps JSON objects as dictionaries, including unknown keys differing only in capitalization.-DateKind Stringkeeps date-looking strings as strings, preserving their original offsets; this prepared PowerShell version supports that option.-NoEnumeratekeeps a request array together even when it has just one member.@(...)collects matches into an array, even for zero or one match.-ceqcompares case-sensitively.-ccontainstests exact membership;-cnotcontainsis its inverse.-nemeans not equal,-gtmeans greater than, and-ormeans either condition.-isnot [array]rejects a value that is not an array.$seen += ...and$changes += ...append to collections. An unknown ID, an unapproved session, a duplicate request or too many attendees throws before any output is written.-gtpermits capacity exactly equal to attendance.[pscustomobject]@{...}collects the known plan fields.ConvertTo-Json -InputObject $changes -Depth 5serializes the complete array; a sufficient depth avoids converting nested objects to display strings. JSON property order and indentation do not matter; array order does. Boolean values differ from numbers, while equal finite numeric spellings such as1and1.0represent the same JSON number.- PowerShell can call .NET methods with
[Type]::Method(...).[System.IO.File]::WriteAllTextwrites the selected text to the selected file path.[Environment]::NewLineadds a line ending, and[System.Text.UTF8Encoding]::new($false)creates UTF-8 encoding without a byte-order mark. This method does not automatically honor WhatIf; in the next lab you will put it behind an explicit authorization branch.
A literal here-string saves the full script: @' opens it and '@ closes it on a line by itself. Single quotes preserve the script's variables while saving. Set-Content -Encoding utf8 replaces only this script file:
Set-Content -Path ./Build-RoomPlan.ps1 -Encoding utf8 -Value @'
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$EventPath,
[Parameter(Mandatory)][string]$PlanPath,
[Parameter(Mandatory)][string]$OutputPath
)
try {
$ErrorActionPreference = 'Stop'
$fullOutput = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath)
if (Get-Item -LiteralPath $fullOutput -Force -ErrorAction SilentlyContinue) {
throw 'Output already exists; choose a new path.'
}
$parent = Split-Path -LiteralPath $fullOutput
if (-not (Test-Path -LiteralPath $parent -PathType Container)) {
throw 'Output parent must already exist.'
}
$event = Get-Content -LiteralPath $EventPath -Raw | ConvertFrom-Json -AsHashtable -DateKind String
$requests = Get-Content -LiteralPath $PlanPath -Raw | ConvertFrom-Json -AsHashtable -DateKind String -NoEnumerate
if ($requests -isnot [array] -or $requests.Count -eq 0) { throw 'Plan must be a nonempty array.' }
$changes = @()
$seen = @()
foreach ($request in $requests) {
if ($seen -ccontains $request.SessionId) { throw "Duplicate session ID: $($request.SessionId)" }
$seen += $request.SessionId
$sessions = @($event.Sessions | Where-Object { $_.SessionId -ceq $request.SessionId })
$rooms = @($event.Rooms | Where-Object { $_.RoomId -ceq $request.RoomId })
if ($sessions.Count -ne 1) { throw "Unknown or ambiguous session ID: $($request.SessionId)" }
if ($rooms.Count -ne 1) { throw "Unknown or ambiguous room ID: $($request.RoomId)" }
if ($event.ApprovedSessionIds -cnotcontains $request.SessionId) { throw "Unapproved session ID: $($request.SessionId)" }
if ($sessions[0].Attendees -gt $rooms[0].Capacity) { throw "Capacity exceeded: $($request.SessionId) -> $($request.RoomId)" }
$changes += [pscustomobject]@{
SessionId = $request.SessionId
OldRoomId = $sessions[0].RoomId
NewRoomId = $request.RoomId
Attendees = $sessions[0].Attendees
Capacity = $rooms[0].Capacity
}
}
$json = ConvertTo-Json -InputObject $changes -Depth 5
[System.IO.File]::WriteAllText($fullOutput, $json + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false))
Write-Output "Saved plan: $fullOutput"
}
catch {
Write-Error ("Plan failed: {0}" -f $_.Exception.Message) -ErrorAction Continue
exit 1
}
'@
Run the validator in a child PowerShell process. -NonInteractive forbids unattended prompts, and -File chooses the script. The child's exit 1 cannot close your interactive session:
pwsh -NoLogo -NoProfile -NonInteractive -File ./Build-RoomPlan.ps1 -EventPath ./event.json -PlanPath ./requests.json -OutputPath ./validated-plan.json
$LASTEXITCODE is the native exit status of the last external program. Read it immediately:
$LASTEXITCODE
0
Reopen the actual artifact:
Get-Content ./validated-plan.json -Raw
It contains both requested rows: s-a, r-old, r-equal, 24, 24; and s-c, r-old, r-large, 18, 40. The original schedule remains unchanged. An existing output is deliberately refused, rather than silently replaced. If you need to rebuild your own plan, remove only validated-plan.json with Remove-Item -LiteralPath ./validated-plan.json, then run the corrected validator again.
Reject a later invalid request as a whole
bad-request.json contains a valid first change followed by an unknown target room. The next command intentionally fails:
pwsh -NoLogo -NoProfile -NonInteractive -File ./Build-RoomPlan.ps1 -EventPath ./event.json -PlanPath ./bad-request.json -OutputPath ./bad-plan-result.json
Expect Plan failed: and an unknown-room explanation. Read the native result immediately:
$LASTEXITCODE
1
Test-Path ./bad-plan-result.json
False
The earlier valid row was not published as a partial plan. Unknown session IDs, unapproved IDs, duplicates and exceeded capacity must also refuse the invocation before writing. An empty or missing request file is not a successful empty job.
For an ordinary misspelled path, use Get-Location and Get-ChildItem to check your directory and names, then return with Set-Location /home/labex/project/room-changes. For a script typo, save the complete corrected here-string again, replacing only your script. Do not edit the source data to make an error disappear. The final check reads the real business plan and original properties, and runs your exact script bytes in isolated events with different IDs. It does not accept a fixed list of initial answers or change your live schedule.
Summary
You used an honest native preview, related sessions to rooms by exact IDs, checked approval and capacity, and saved a complete validated business plan. Capacity equality was accepted, same-title records stayed distinct, and a later invalid request produced no partial plan. The original event and notes remained unchanged; the next lab applies a whole validated plan through native confirmation.



