“The most effective debugging tool is still careful thought, coupled with judiciously placed print statements.”
— Brian Kernighan, Unix for Beginners (1979)
Summary
PowerShell automation is the Windows-first layer between orchestration and raw command execution in data platforms.
Use these scripts to validate inbound data, reshape extracts, call APIs, and guard scheduled jobs against common failure modes.
Expect explicit checks for schema drift, nulls, duplicate keys, checksum mismatches, lagging consumers, and low-disk conditions.
Treat this page as PowerShell-first reference material; use the Bash companion page when the runtime is Linux-native.
Glossary
PowerShell script (.ps1)
A plain-text file containing PowerShell code, usually saved with the .ps1 extension and executed by pwsh or powershell.exe.
The standard unit for Windows automation: scheduled jobs, file handling, system administration, API calls, and SQL Server operations.
Windows may still block unsigned scripts until Set-ExecutionPolicy RemoteSigned -Scope CurrentUser is configured, and Task Scheduler can fail under a different execution context than an interactive shell.
$ErrorActionPreference
A preference variable that controls how non-terminating PowerShell errors are handled. The default value, Continue, reports the error and keeps executing.
Commonly set to Stop near the top of a script so cmdlet and provider errors become terminating errors that can halt execution or be caught reliably.
It applies to PowerShell errors, not native process failures. Commands such as gcloud, python, or sqlcmd still require explicit $LASTEXITCODE checks.
$LASTEXITCODE
An automatic variable that stores the exit code returned by the most recently completed native executable.
The primary way to detect failure from non-PowerShell tools such as gcloud, bq, sqlcmd, az, or python inside a PowerShell script.
In Bash, $? holds the previous command status. In PowerShell, $? and $LASTEXITCODE are not interchangeable; $LASTEXITCODE is the native-process signal you need here.
Set-StrictMode
A cmdlet that makes loose or ambiguous behaviors fail fast, including references to uninitialized variables and some invalid property or method usage.
Useful in automation code where silent coercion or accidental null access would otherwise stay hidden until a later stage.
Set-StrictMode affects the current scope and child scopes created afterward, so place it near the top of the script before defining functions.
try / catch / finally
PowerShell’s structured exception-handling construct: try runs guarded code, catch handles terminating errors, and finally runs cleanup code whether an error occurred or not.
The standard pattern for controlled failure handling and guaranteed cleanup of temp files, locks, connections, or streams.
catch only handles terminating errors. Without $ErrorActionPreference = 'Stop' or -ErrorAction Stop, many cmdlet failures emit an error record and execution continues.
Execution policy
A PowerShell security feature that determines which scripts are allowed to run under a given scope and trust model, using policies such as Restricted, RemoteSigned, or Bypass.
Important for script deployment and scheduled automation because a valid .ps1 file can still be blocked before any code runs.
Bash has no comparable platform-wide script execution policy; Linux and macOS rely primarily on file permissions and the selected interpreter.
Import-Csv / Export-Csv
Cmdlets for converting between CSV text and structured objects. Import-Csv reads rows into objects with named properties; Export-Csv writes objects back to CSV with a header row.
They let automation address columns by name instead of positional parsing, which is usually more readable and more robust to schema changes.
Bash has no native CSV parser with schema-aware property access. Simple files can be handled with awk, but quoted fields and embedded commas usually require a real CSV parser such as Python’s.
Invoke-RestMethod
A cmdlet that sends HTTP or HTTPS requests and automatically converts common response types such as JSON or XML into PowerShell objects.
Commonly used for API polling, token acquisition, metadata retrieval, and paginated ingestion workflows without manual response parsing.
The usual Bash equivalent is curl for transport plus jq for JSON parsing. Invoke-RestMethod combines those steps for many API cases.
NDJSON (Newline-Delimited JSON)
A text format in which each line is an independent JSON value, most often one JSON object per line. It is also commonly called JSON Lines (.jsonl).
Used for streaming and large-scale processing because records can be produced and consumed incrementally without loading an entire JSON array into memory.
jq -c '.[]' input.json is a common Bash pattern for emitting one compact object per line from a JSON array. In PowerShell, emit one object at a time and serialize each record individually.
Exponential backoff
A retry strategy in which the delay between attempts increases, typically by doubling after each failure, often with an upper limit and optional random jitter.
Used to handle transient failures without overwhelming an unstable upstream service or creating synchronized retry spikes across many workers.
The algorithm is the same in Bash, PowerShell, Python, or any other language even though the control-flow syntax differs.
Mutex (named mutex)
An operating-system synchronization primitive that allows only one holder at a time. A named mutex can be shared across processes, and sometimes across sessions, depending on how it is created.
Used to prevent overlapping executions of the same scheduled task or script when concurrent runs would corrupt state or duplicate work.
A naive lock-file pattern such as if (Test-Path lock) { exit } ; New-Item lock has a race between the check and the create. Mutex acquisition is designed to avoid that gap.
Invoke-Sqlcmd
A cmdlet from the SqlServer PowerShell module that executes Transact-SQL against SQL Server and returns results as structured rows rather than plain console text.
Useful when a script needs direct SQL execution with object-oriented output that can be filtered, inspected, or exported without manual text parsing.
In shell workflows, sqlcmd usually emits text that must be parsed afterward. Invoke-Sqlcmd is more convenient when the rest of the workflow is already object-based in PowerShell.
Task Scheduler
The built-in Windows job scheduler that launches tasks on a time schedule or in response to triggers such as startup, logon, or system events.
The standard Windows mechanism for running unattended scripts, recurring automation, and operational jobs outside an interactive shell session.
Scheduled tasks often run with a different user context, environment, working directory, and profile state than an interactive terminal, so assumptions that hold manually can still fail when scheduled.
PowerShell is one of the four core languages of the data engineer alongside SQL, Python, and a JVM language. These scripts automate the repetitive, error-prone tasks that sit between pipeline orchestration and raw shell commands: validating incoming files, transforming formats, querying APIs, checking database health, managing cloud resources, parsing logs, and wiring up scheduling.
Every script in this page follows the defensive scripting patterns documented in defensive-scripting and uses the command chaining operators explained in command-chaining. The Bash equivalent of every script exists at bash-automation.
The catalog below follows the same path most data jobs do: validate the input, reshape it, call external systems, verify the load, and then harden the runtime around retries and scheduling.
flowchart LR
A[File Intake<br>& Validation] --> B[Data<br>Transformation]
B --> C[API<br>Interaction]
C --> D[Database<br>Operations]
D --> E[GCP Cloud<br>Operations]
E --> F[Log Parsing<br>& Monitoring]
F --> G[Environment<br>& Pre-flight]
G --> H[Scheduling<br>& Orchestration]
style A fill:#292e42,stroke:#7aa2f7
style B fill:#292e42,stroke:#7aa2f7
style C fill:#292e42,stroke:#7aa2f7
style D fill:#292e42,stroke:#7aa2f7
style E fill:#292e42,stroke:#9ece6a
style F fill:#292e42,stroke:#9ece6a
style G fill:#292e42,stroke:#9ece6a
style H fill:#292e42,stroke:#9ece6a
File intake and validation
Incoming data is the single largest source of pipeline failures. A file that arrives with missing columns, null values in mandatory fields, or duplicate keys will propagate errors silently through every downstream transformation. These scripts catch problems at the gate, before any processing begins.
Validation scripts
CSV header validator
Before any transform or load accepts a new file. It is typically triggered when an incoming file must prove freshness, schema, or row integrity before downstream processing continues. Compares the header row of an incoming CSV file against a golden schema file that defines the expected column names and order. If the headers do not match exactly, the script prints the diff and exits with a non-zero code, preventing the pipeline from processing a malformed file.
Header parsing must be schema-aware
This check is trustworthy only when the incoming header is parsed as CSV rather than treated as a raw byte string. Delimiter drift, BOM-prefixed first columns, or quoted commas can turn a naive split into a false drift result.
OK - headers match schema for signals_daily_sample.csv
Null and empty field scanner
Before any transform or load accepts a new file. It is typically triggered when an incoming file must prove freshness, schema, or row integrity before downstream processing continues. Scans a CSV file for rows where mandatory columns contain empty values. The script accepts a comma-separated list of column names that must not be empty. It reports every offending row number and the column that failed, making it easy to trace the problem back to the source system.
Normalize business sentinels such as NULL, N/A, or -9999 before the emptiness test if the upstream system uses placeholders instead of actual blanks. Otherwise the scanner can pass rows that are operationally null but not syntactically empty.
Scan the intentionally broken signals_daily_missing.csv fixture for empty symbol and recommendation_mean fields.
Row 5: column 'recommendation_mean' is emptyRow 9: column 'symbol' is empty
Duplicate key detector
Before any transform or load accepts a new file. It is typically triggered when an incoming file must prove freshness, schema, or row integrity before downstream processing continues. Checks a CSV file for duplicate values in a specified key column. Data engineers loading into warehouses with primary key constraints need to detect duplicates before the load, not after a constraint violation crashes the job.
Match the warehouse key semantics
Group-Object only sees the exact strings in the file. If the target key is case-insensitive or trims trailing spaces, normalize the input first so values such as ABC, abc, and ABC do not survive the file check only to collide at load time.
Group the duplicate-symbol fixture and fail when a signals_daily symbol appears more than once.
Before any transform or load accepts a new file. It is typically triggered when an incoming file must prove freshness, schema, or row integrity before downstream processing continues. Monitors a landing directory for the arrival of an expected file within a deadline. Data pipelines that depend on upstream file drops need an early alert when the file is late, rather than discovering the gap hours later when a downstream job fails.
Check that the landing folder contains a fresh signals_daily_*.csv drop within the last 60 minutes.
OK - 1 file(s) found, newest: signals_daily_20260414.csv
Data transformation
Once a file passes validation, it often needs reshaping before it can be loaded into a target system. These scripts handle the most common format conversions and structural changes that data engineers perform daily: selecting columns, splitting oversized files, and converting between CSV and JSON-oriented formats.
Transformation scripts
CSV column extractor and reorderer
After validation and before the target load step. It is typically triggered when a validated dataset must be reshaped into the format the next system expects. Selects specific columns from a CSV file and writes them in a new order. This is essential when a source system delivers 50 columns but the target table only needs 5, or when the column order must match a schema definition.
Project four warehouse-facing columns from the sampled signals_daily extract into a new CSV.
OK - wrote 12 rows with 4 columns to signals_daily_projection.csv
Large CSV splitter
After validation and before the target load step. It is typically triggered when a validated dataset must be reshaped into the format the next system expects. Splits a large CSV file into smaller chunks of N rows each, preserving the header row in every chunk. Break a large extract into smaller, repeatable batches that are easier to load, retry, or parallelize downstream.
Split by row boundary, not by byte count
CSV quoting means a file-size split can start in the middle of a record. Re-emitting complete rows is slower than a raw file split, but it preserves a valid CSV contract for retries and parallel loads.
Split the full data/signals_daily.csv extract into 200-row chunks under data/powershell-automation/split.
After validation and before the target load step. It is typically triggered when a validated dataset must be reshaped into the format the next system expects. Converts a JSON array of flat objects into a CSV file. Many APIs return JSON, but warehouse bulk-load tools (BigQuery bq load, PostgreSQL \COPY) expect CSV. PowerShell’s ConvertFrom-Json and Export-Csv handle this conversion natively.
This pattern assumes a flat object per row. If the payload contains nested arrays or objects, flatten or project the structure explicitly before exporting to CSV.
Flat CSV is a lossy target
Nested arrays, maps, and repeated attributes usually need an explicit projection rule. When the upstream API evolves frequently, keep the raw JSON alongside the flattened CSV so downstream reprocessing does not depend on today’s projection choices.
Flatten the sampled dim_country JSON array into a CSV that is ready for bulk-load tooling.
OK - wrote 8 rows with 2 columns to dim_country_sample.csv
CSV to NDJSON converter
After validation and before the target load step. It is typically triggered when a validated dataset must be reshaped into the format the next system expects. Converts a CSV file to newline-delimited JSON (NDJSON), a common format for JSON loads, streaming-style ingestion, and many modern data tools. Each CSV row becomes a single JSON object on its own line.
Cast types before strict JSON loads
Import-Csv returns strings for every field. If the downstream system enforces numeric, date, or boolean types, cast them before writing NDJSON so type drift is caught in the transform step rather than at the destination.
Convert the sampled signals_daily CSV into NDJSON records for streaming or API-based loads.
OK - wrote 12 NDJSON records to signals_daily_sample.ndjson
API interaction
API-bound automation fails most often at the network boundary: transient status codes, pagination, token expiry, or incomplete downloads. These examples focus on resilient retrieval, stateful authentication, and artifact integrity.
API scripts
REST GET with retry and backoff
During extraction or integration work that crosses an HTTP boundary. It is typically triggered when the pipeline depends on an external API or downloaded artifact. Fetches a URL with configurable retry count and exponential backoff. Transient failures (network blips, 502/503 responses) are the norm when calling external APIs. Without retries, a single timeout kills an entire pipeline run.
Retry scope must stay idempotent
Automatic retries are safe only when replaying the same request cannot create extra side effects. HTTP GET is designed to be safe and idempotent; write operations need a separate idempotency design before the same wrapper is reused.
Reuse the same loop for a state-changing request
A timeout after the remote side commits can still produce a duplicate write on the next attempt.
while ($attempt -lt $MaxRetries) { Invoke-RestMethod -Uri $Url -Method Post -Body $payload}
Keep generic retries on idempotent reads
Use the wrapper around metadata reads, downloads, or writes protected by an idempotency key.
OK - HTTP 200 after 1 attempt(s)Saved response to signals_daily_table.json
Paginated API fetcher
During extraction or integration work that crosses an HTTP boundary. It is typically triggered when the pipeline depends on an external API or downloaded artifact. Collects all pages from a cursor-based or offset-based paginated API into a single output file. Most APIs limit response size to 100–1000 records per call. This script follows the pagination chain until no next cursor is returned, merging all results into one JSON array.
Persist the resume token when the crawl matters
If a paginated extraction spans minutes or hours, store the last successful page token or cursor after each page. Restarting from page 1 after a late failure can duplicate data, re-read expensive endpoints, or cross an upstream retention window.
Walk the paginated BigQuery tables API for stoxx_silver two tables at a time and persist the combined JSON.
Page 1 fetched, 2 table(s), nextPageToken returnedPage 2 fetched, 2 table(s), nextPageToken returnedPage 3 fetched, 2 table(s)OK - fetched 3 page(s), 6 total records to stoxx_silver_tables.json
Bearer token refresh wrapper
During extraction or integration work that crosses an HTTP boundary. It is typically triggered when the pipeline depends on an external API or downloaded artifact. Obtains an OAuth2 bearer token using client credentials grant, caches it in a variable, and re-authenticates when the token expires or a 401 response is received. This pattern is standard for service-to-service API calls where tokens have a limited TTL (typically 3600 seconds).
Refreshing a few minutes before nominal expiry is deliberate. It absorbs clock skew between the runner and the issuer so a token does not expire mid-request even though the local cache still thinks it is valid.
Refresh a cached bearer token, then call the BigQuery datasets API with the active gcloud credential.
During extraction or integration work that crosses an HTTP boundary. It is typically triggered when the pipeline depends on an external API or downloaded artifact. Downloads a file and verifies its SHA-256 hash against an expected value. Data integrity is non-negotiable when downloading datasets, model artifacts, or binary dependencies. A corrupted file that passes silently can produce wrong results that are far harder to detect than a failed download.
Checksums need an immutable artifact reference
A matching hash proves the bytes you downloaded match the bytes you hashed. It does not prove the object path was stable while you downloaded it. Mutable object names need a version, generation, or signed manifest strategy as well as a checksum.
Validate a shared object name after the fact
Another writer can replace the object between metadata lookup, download, and later reuse of the same path.
OK - downloaded eurostoxx50_ohlcv.csv (4682 bytes), checksum verified
Database operations
These examples assume SQL Server tooling because Invoke-Sqlcmd returns structured rows directly into PowerShell. If the same workflow targets another engine, keep the operational pattern and swap only the client and authentication layer.
Database scripts
Database connectivity health check
Before, during, or immediately after a database-backed load step. It is typically triggered when a database-dependent run needs readiness, extraction, or post-load validation. Tests whether a database is reachable and responsive by executing a trivial query and measuring the round-trip time. This is the first check in any pipeline that depends on a database — there is no point starting a multi-hour ETL job if the target is unreachable.
Run a live SELECT 1 against stoxx on localhost,1434 and report the measured round-trip time.
Before, during, or immediately after a database-backed load step. It is typically triggered when a database-dependent run needs readiness, extraction, or post-load validation. Executes a SQL file against a database and writes the result set to a CSV file. This is the standard extraction step in any EL(T) pipeline — pull data from a source database into a portable format for transfer or transformation.
Project business columns before Export-Csv
Invoke-Sqlcmd returns DataRow objects. Selecting the output columns explicitly keeps the extract contract stable and avoids leaking row metadata or later query changes into the CSV.
Execute the saved stoxx_eurostoxx_latest.sql query and export the result set to CSV.
OK - exported 12 rows with 4 columns to stoxx_eurostoxx_latest.csv
Row count reconciliation
Before, during, or immediately after a database-backed load step. It is typically triggered when a database-dependent run needs readiness, extraction, or post-load validation. Compares the number of data rows in a source CSV file against the row count in the target database table after a load. A mismatch means rows were lost or duplicated during the load — either case is a data quality incident that must be caught immediately.
Matching counts can still hide drift
Count parity proves only that both sides contain the same number of rows. It does not prove that the same business keys, dates, or measures survived the handoff.
Approve the load on count parity alone
This still passes when duplicated keys or shifted measures preserve the row count.
if ($fileRows -eq $dbRows) { Write-Output 'counts match'}
Pair counts with a control total or key check
Reconcile one or more business measures so the verification fails on silent content drift.
$fileTotal = (Import-Csv $CsvFile | Measure-Object -Property close -Sum).Sum$dbTotal = (Invoke-Sqlcmd -ServerInstance 'localhost,1434' -Database 'stoxx' -Username 'sa' -Password 'EsgDev2026Pass1' -TrustServerCertificate -Query 'SELECT SUM([close]) AS total_close FROM silver.eurostoxx50_ohlcv WHERE [date] = ''2026-04-07'';').total_close
Compare the exported CSV file with the live row count returned by the paired stoxx_eurostoxx_latest_count.sql statement.
OK - 12 rows in stoxx_eurostoxx_latest.csv match 12 rows returned by stoxx_eurostoxx_latest_count.sql
GCP cloud operations
gcloud, gsutil, and bq are fully cross-platform. In PowerShell, pipe JSON output to ConvertFrom-Json; on Linux, pipe the same JSON output to jq. The CLI flags and service behavior stay the same across both shells.
Cloud automation scripts
GCS stale object reporter
During cloud operations that interrogate or guard Google Cloud resources. It is typically triggered when the job needs a direct operational check against Google Cloud resources. Lists objects in a GCS bucket that are older than a specified number of days. Stale data accumulates in landing buckets when upstream systems stop cleaning up, leading to unexpected storage costs and confusion about which files are current. This script surfaces objects past their expected retention.
List real objects in gs://stoxx-bq-bucket/export that are older than the retention threshold.
gs://stoxx-bq-bucket/export/eurostoxx50_ohlcv-000000000000.csv.gz 1548 bytes 2 days oldgs://stoxx-bq-bucket/export/eurostoxx50_ohlcv-000000000000.parquet 7155 bytes 2 days old--- Objects older than 1 day(s) listed above ---
GCS stage and promote with checksum verification
Before a file leaves the landing zone and becomes visible to downstream BigQuery loads or consumers. It is typically triggered when a local extract or transformed file is ready to publish into the project buckets. Uploads a local file from C:\Users\aperi\My Drive\VAULT\data\powershell-automation\incoming into stoxx-stage-bucket, compares the local and remote MD5 hashes, then copies the verified object into stoxx-bq-bucket. Separate file arrival from file promotion so corrupt uploads, partial rewrites, and wrong object versions are caught before production readers see them.
Direct publishing can overwrite concurrent runs
A stable destination object name is convenient, but it is also where retries and overlapping schedules collide. Checksums prove content integrity, not publish safety.
Promote into a shared stable object path
The copy succeeds technically, but a retried run can replace another run’s output without either side noticing.
This pattern keeps staging and consumption distinct. Get-FileHash calculates the local checksum, gcloud storage objects describe returns the remote checksum and generation number, and the script refuses promotion unless the staged object matches the local file byte-for-byte.
Upload the sample CSV to stoxx-stage-bucket, verify the checksum, and promote the same verified object into stoxx-bq-bucket.
Local MD5: ed8c817608799befe9121aae5a40e7b1Stage object: powershell-automation/signals_daily_sample.csv generation 1776198829563408 md5 ed8c817608799befe9121aae5a40e7b1Promote object: powershell-automation/signals_daily_sample.csv generation 1776198834463675 md5 ed8c817608799befe9121aae5a40e7b1Checksum verified across stage and promoted copies.
BigQuery dry-run cost estimator
During cloud operations that interrogate or guard Google Cloud resources. It is typically triggered when the job needs a direct operational check against Google Cloud resources. Estimates the bytes that a BigQuery query will scan before actually running it. Estimate scan volume before the real query runs so cost surprises and missing partition filters are caught early.
Row caps do not bound scan cost
In BigQuery, LIMIT changes the returned rows, not necessarily the bytes scanned. Cost control comes from pruning partitions, reducing referenced columns, and setting an explicit bytes ceiling.
Assume LIMIT makes the query cheap
This can still scan a large table even though only a few rows come back.
'SELECT * FROM `bq-wh-nb.stoxx_gold.signals_daily` LIMIT 10' | bq query --use_legacy_sql=false
Dry-run and pin a maximum bytes budget
Validate the scan size before execution and fail if the query would exceed the cost boundary.
$query = 'SELECT * FROM `bq-wh-nb.stoxx_gold.signals_daily` WHERE signal_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)'$query | bq query --use_legacy_sql=false --dry_run --maximum_bytes_billed=104857600
BigQuery charges per byte scanned ($6.25/TB in on-demand pricing as of 2026). Running a --dry_run first prevents expensive mistakes like querying a multi-terabyte table without a partition filter.
Dry-run the saved BigQuery statement in data/powershell-automation/sql and estimate scan cost before execution.
BigQuery load job with polling and row-count verification
After a staged object has passed checksum verification and is ready to enter a BigQuery dataset. It is typically triggered when a batch file is present in GCS and the next workflow step is to load it into BigQuery without guessing whether the job finished cleanly. Starts an asynchronous bq load job from gs://stoxx-stage-bucket/powershell-automation/signals_daily_sample.csv into stoxx_bronze.powershell_automation_signals_load, polls the job state with bq show -j, then runs a verification query stored under data\powershell-automation\sql. Turn an opaque background load into a deterministic step that exposes job completion, row count, date range, and symbol cardinality before downstream SQL reads the table.
Production loads need a pinned schema
Schema autodetection is useful for quick validation, but it is a weak contract for repeatable production loads. Column modes, record types, and subtle type changes are much easier to control with an explicit schema file.
Let BigQuery infer the contract
This works until an upstream file changes enough for BigQuery to infer a different type or nullable shape.
The destination table is recreated with WRITE_TRUNCATE on each run, so the verification query always reflects the current file, not historical residue. The post-load query checks four operational facts: total rows loaded, earliest and latest signal_date, and how many distinct symbol values reached the table.
Launch a live BigQuery load job, poll until it reaches DONE, and verify the loaded table with a saved SQL file.
Poll 1 - state DONEJobId: bqjob_r3779d54b1abc6b9b_0000019d8db42833_1Loaded rows: 12Signal date range: 2026-03-04 to 2026-03-04Distinct symbols: 12
BigQuery schema drift checker
Immediately before a load job or schema-sensitive transform that expects a stable file contract. It is typically triggered when a new feed revision arrives, a producer changes a header row, or a target table has been altered in BigQuery. Reads the header fixture C:\Users\aperi\My Drive\VAULT\data\powershell-automation\schemas\signals_daily_drift_header.csv, fetches the live BigQuery schema for stoxx_silver.signals_daily, and compares both column lists in PowerShell. Fail fast on file-versus-table mismatches so a bad header never reaches bq load, where the failure message usually arrives later and with less context.
This check treats missing and extra columns as different failure modes. Missing columns mean the file cannot satisfy the table contract; extra columns usually indicate upstream schema expansion that downstream code has not yet approved.
The example compares names only. Production gates should also compare data types and modes when downstream SQL depends on a specific numeric precision, repeated field shape, or nullable contract.
Compare a drifted local header file to the live stoxx_silver.signals_daily schema and emit a non-zero drift result.
DRIFT - schema mismatch against bq-wh-nb:stoxx_silver.signals_dailyMissing in file: <none>Extra in file: ingested_at
BigQuery table freshness checker
On a schedule after ingestion windows close or before dependent marts assume the latest partition is available. It is typically triggered when a table has a freshness SLA expressed in business-date lag rather than just job completion. Runs the saved query C:\Users\aperi\My Drive\VAULT\data\powershell-automation\sql\bq_signals_freshness.sql against stoxx_silver.signals_daily, compares the returned lag_days to a threshold, and emits a pass/fail status. Convert a date field inside the table into an operational readiness check so downstream jobs can stop on stale data instead of processing yesterday’s or last week’s snapshot.
The query returns three values that matter together: the latest business date present, how many days that date lags CURRENT_DATE(), and how many rows are in scope. A table can have recent row counts yet still be stale if the latest business date stops moving.
Evaluate the saved freshness query and fail only when the live lag exceeds the configured SLA threshold.
OK - stoxx_silver.signals_daily latest signal_date 2026-04-08 is 6 day(s) old (threshold: 7)Rows monitored: 635
Pub/Sub backlog monitor
During cloud operations that interrogate or guard Google Cloud resources. It is typically triggered when the job needs a direct operational check against Google Cloud resources. Checks the number of undelivered messages across one or more Pub/Sub subscriptions and alerts if any exceed a threshold. A growing backlog means consumers are falling behind — this is often the first sign of a processing bottleneck or a crashed subscriber.
Read the live Pub/Sub backlog metric from Cloud Monitoring for the project subscription.
OK - eventarc-europe-west1-stoxx-firestore-control-written-sub-850: 0 undelivered messages
Pub/Sub backlog trend monitor
When a single backlog point is not enough and you need to know whether a subscription is recovering, flat, or repeatedly building debt over time. It is typically triggered when operators want a windowed signal before paging on transient spikes or overlooking a slowly growing backlog. Calls the Cloud Monitoring timeSeries API for pubsub.googleapis.com/subscription/num_undelivered_messages, aligns points into five-minute maxima over a six-hour window, then evaluates the maximum, average, and number of non-zero samples. Detect sustained subscriber lag instead of reacting to one instantaneous sample that may already have cleared by the time the check runs.
Pair backlog count with message age
A growing count and a growing oldest-unacked age together indicate subscribers are not keeping up. Count alone can spike transiently during bursts; age shows whether the backlog is actually aging toward retention risk.
This version is stricter than the point-in-time monitor because it requires both a threshold breach and repeated non-zero samples before it alerts. That reduces noise during short-lived bursts while still catching a consumer that remains behind for multiple alignment windows.
Read the aligned backlog history for the Eventarc subscription and summarize whether the backlog is sustained or transient.
Window: 6 hour(s), samples: 1Latest point: 2026-04-14T20:36:19.8949970Z backlog 0Max backlog: 0, average backlog: 0, non-zero samples: 0OK - no sustained backlog detected for eventarc-europe-west1-stoxx-firestore-control-written-sub-850
Service account key age checker
During cloud operations that interrogate or guard Google Cloud resources. It is typically triggered when the job needs a direct operational check against Google Cloud resources. Lists all keys for a service account and flags any that are older than a specified number of days (default: 90). Surface user-managed keys that should be rotated before they become a security exception or a forgotten long-lived credential.
Keys should be the exception
User-managed service account keys work, but they turn IAM access into a copyable file that can survive in downloads, temp folders, CI caches, or old workstations long after the workload has changed.
Create and export a key file
This moves the credential boundary from IAM into filesystem hygiene and secret distribution.
gcloud iam service-accounts keys create sa-key.json --iam-account=$ServiceAccountEmail$env:GOOGLE_APPLICATION_CREDENTIALS = (Resolve-Path '.\sa-key.json')
Impersonate at runtime
Prefer short-lived credentials that are minted when needed and never written as reusable key files.
Google recommends rotating service account keys every 90 days. Forgotten user-managed keys are a security risk because they often outlive the systems that created them.
Inspect user-managed keys on bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com and flag keys older than 20 days.
ROTATE - key 3166c79513e7... created 03/22/2026 16:27:38OK - key b228f14a7cc8... created 04/05/2026 07:34:04
Data movement pipelines
Most production automation moves files between systems more often than it performs complicated in-memory transformations. These patterns show the handoff points explicitly: a local file published to GCS, a local file loaded straight into BigQuery, a host file streamed into SQL Server, a JSON file upserted into Firestore, and a chained pipeline that crosses all four targets in sequence.
Destination loads
These examples start from local files under C:\Users\aperi\My Drive\VAULT\data\powershell-automation. Each script finishes with a live destination-side check so the movement step proves that the target now contains the expected data instead of only assuming the upload succeeded.
Local file to GCS object
When a local export, transformed file, or partner drop must be made available to cloud consumers through a bucket path. It is typically triggered when a PowerShell run has produced a file on the host and the next stage expects a GCS object instead of a local path. Uploads transformed\signals_daily_projection.csv from the vault data directory into gs://stoxx-stage-bucket/powershell-automation/local-file-upload/ and then reads the object metadata back from GCS. Publish a host-side file into shared cloud storage while capturing the exact object name, generation, and byte size that downstream jobs should reference.
This is the direct host-to-bucket pattern. gcloud storage cp performs the upload and gcloud storage objects describe confirms which immutable object generation now exists in the bucket.
Upload the local projection CSV into stoxx-stage-bucket and confirm the created object metadata.
When a small or medium file already exists on the host and you want an immediate table load without first staging to GCS. It is typically triggered when a PowerShell job has produced a CSV locally and the next step is an ad hoc or agent-local BigQuery load. Loads transformed\signals_daily_projection.csv directly into bq-wh-nb:stoxx_bronze.powershell_automation_local_file_load, then runs the saved verification query sql\bq_local_file_verify.sql. Turn a local CSV into a queryable BigQuery table in one step and verify the table shape with real row-level facts from the destination.
This pattern is useful on Windows build agents and scheduled runners when the file is already present locally. For large or shared feeds, stage to GCS first; for local-only outputs, direct bq load removes one hop.
Direct local loads are an agent-local convenience
They are useful for small files and one-hop automation, but they are weaker than GCS-backed loads for replay, provenance, and multi-runner portability. Promote to GCS first when more than one machine or retry boundary must be able to see the same source artifact.
Load the local projection CSV straight into BigQuery and verify the resulting table with the saved SQL file.
When SQL Server is the immediate next system but the source file exists only on the host running PowerShell. It is typically triggered when a CSV extract has landed on the Windows runner and the target SQL Server instance cannot read that host path directly. Reads transformed\signals_daily_projection.csv, prepares dbo.powershell_automation_local_file_load with sql\stoxx_local_file_load_setup.sql, streams the rows into stoxx over TDS with SqlBulkCopy, and validates the result with sql\stoxx_local_file_load_verify.sql. Load a host-local file into SQL Server without depending on SQL Server service account access to the host filesystem or a container bind mount.
Because stoxx is running in the stoxx-db container, SQL Server cannot see arbitrary host file paths like C:\Users\aperi\My Drive\VAULT\data\.... PowerShell must read the file on the host and push the rows over the database connection. SqlBulkCopy is the practical high-throughput pattern for that handoff.
Keep bulk load semantics explicit
SqlBulkCopy is materially safer than row-by-row insert loops for throughput and retry visibility. For larger loads, add batch size, timeout, and destination transaction choices explicitly rather than relying on module defaults.
Stream the local projection CSV into stoxx.dbo.powershell_automation_local_file_load and verify the loaded rows with the saved SQL file.
When the destination is a document store and the source file already exists as local JSON on the runner. It is typically triggered when a process has produced a small dimension, control, or status file that should become Firestore documents. Reads json\dim_country_sample.json, authenticates with gcloud auth print-access-token, and upserts one document per iso_alpha2 value into the Firestore Native database projects/bq-wh-nb/databases/main. Publish structured local JSON into Firestore with deterministic document IDs so repeated runs remain idempotent.
Firestore does not offer a simple gcloud equivalent of bq load for arbitrary local JSON arrays, so PowerShell acts as the adapter: it reads the file, maps each element into Firestore’s document format, and calls the REST API with PATCH to create or update documents in place.
Firestore write scaling depends on document IDs and indexed fields
Deterministic IDs are good for idempotency, but high-volume collections should avoid monotonically increasing document IDs or unnecessary indexing on sequential fields such as timestamps. This example stays far below Firestore’s request-size and write-hotspot boundaries.
Upsert the local country JSON file into the powershell_automation_country_load collection and confirm the live document count.
Source rows: 8Documents in collection: 8Collection: powershell_automation_country_load
Chained pipelines
Real orchestration usually crosses multiple systems in one run. The key is to make each handoff explicit, persist intermediate artifacts where they matter, and validate every destination before advancing to the next hop.
GCS to SQL Server to BigQuery to Firestore
When a single automation run must ingest a staged cloud file, land it in SQL Server, publish a relational summary into BigQuery, and expose the run result as a document for downstream event-driven consumers. It is typically triggered when a bucket object has arrived and the operational requirement is a multi-system handoff rather than a single-target load. Downloads gs://stoxx-stage-bucket/powershell-automation/signals_daily_sample.csv into landing\chain_signals_daily_sample.csv, loads the rows into stoxx.dbo.powershell_automation_chain_stage, exports a one-row SQL summary to exports\chain_signal_summary.csv, loads that summary into BigQuery, and patches a Firestore run-status document. Demonstrate how PowerShell acts as the control plane between storage, relational, analytical, and document destinations while preserving a verifiable state transition at each step.
Multi-hop loads need a shared run ID
Once one automation run touches storage, SQL Server, BigQuery, and Firestore, partial success is normal operational state rather than an edge case. Without a shared identifier, replay and cleanup devolve into guesswork.
Update each hop independently
Later operators cannot prove which table rows, files, and Firestore document belong to the same pipeline execution.
This is not a direct service-to-service copy. PowerShell performs every boundary crossing deliberately: GCS object to host file, host file to SQL Server table, SQL summary file to BigQuery table, and BigQuery result to a Firestore document. That explicit choreography is what makes retries, validation, and alerting practical in production automation.
Run the full chained handoff from a GCS object through SQL Server and BigQuery into a Firestore status document.
Pipeline logs contain the earliest signal of problems: error spikes, latency changes, and unexpected patterns. These scripts extract actionable information from flat or structured logs without requiring a full observability stack.
Log analysis scripts
Error rate calculator
When a run has produced logs and you need fast operator feedback. It is typically triggered when operational decisions need to be driven from log content rather than raw file inspection. Counts occurrences of each log level (ERROR, WARN, INFO) in a log file and reports percentages. An error rate above 5% is typically cause for investigation; above 10% indicates a systemic problem. This script provides the quick triage numbers that determine whether to escalate.
Calculate error, warning, and info rates from the captured pipeline.log fixture.
When a run has produced logs and you need fast operator feedback. It is typically triggered when operational decisions need to be driven from log content rather than raw file inspection. Extracts log entries from an NDJSON (newline-delimited JSON) log file that match a specified severity level and fall within a time window. Modern applications emit structured logs in JSON format. Filtering these with Select-String loses the structure — ConvertFrom-Json preserves it and enables precise time-range queries.
Filter the NDJSON log fixture for ERROR entries inside the selected UTC time window.
{ "timestamp": "2026-04-14T08:00:11Z", "level": "ERROR", "message": "First webhook notification attempt timed out", "service": "scheduler-wrapper"}{ "timestamp": "2026-04-14T08:00:20Z", "level": "ERROR", "message": "Checksum validation failed on stale local copy", "service": "artifact-verifier"}--- 2 ERROR entries between 2026-04-14T08:00:10Z and 2026-04-14T08:00:21Z ---
Log rotation and compression
When a run has produced logs and you need fast operator feedback. It is typically triggered when operational decisions need to be driven from log content rather than raw file inspection. Compresses log files older than N days and deletes those older than M days. Without rotation, log directories grow unbounded until they fill the disk and crash the application. This script implements the two-stage lifecycle (compress → delete) that works on any Windows system without external tools.
Rotate only closed files
Compression is a storage operation, not a log-writing primitive. Rotating a file that is still open can produce partial archives or break the writer depending on how the application holds the handle.
Compress whatever matches *.log
This is unsafe when the current process still writes to the same path.
These scripts run before a pipeline starts to verify that the execution environment is correctly configured. A missing CLI, unset credential, or exhausted disk is cheaper to reject up front than to recover after partial work.
Pre-flight scripts
Dependency checker
Immediately before the job commits to work on the current host. It is typically triggered when the runtime environment must be validated before the main workload starts. Verifies that all required command-line tools are installed and available on $env:PATH before a pipeline runs. This prevents the frustrating scenario where a job runs for 30 minutes before failing because jq is not installed on the new build agent.
Binary presence is only the first gate
A dependency can exist on PATH and still be unusable because the active account, project, module version, or scheduler environment is wrong. Pair this check with one credential-aware probe for the systems that actually matter to the job.
Verify that the local PowerShell, container, database, and Google Cloud CLI dependencies are all on PATH.
Immediately before the job commits to work on the current host. It is typically triggered when the runtime environment must be validated before the main workload starts. Parses a .env file and exports each key-value pair as an environment variable, skipping comments and blank lines. Environment variables are the standard way to pass configuration to scripts and containers without hardcoding secrets. This loader makes .env files usable outside of Docker Compose.
Do not commit .env files to version control. Add them to .gitignore and prefer runtime secret retrieval, such as gcloud secrets versions access, for production credentials.
Load the example .env file under data/powershell-automation/env into the current process environment.
OK - loaded 5 variable(s) from powershell-automation.env
Disk space pre-flight
Immediately before the job commits to work on the current host. It is typically triggered when the runtime environment must be validated before the main workload starts. Checks all local drives and aborts if any exceed a usage threshold (default: 80%). A full disk during a pipeline run causes silent data corruption, truncated files, and database crashes. This check takes milliseconds and prevents hours of recovery.
Check the volume that holds temporary files and the volume that receives final outputs. On Windows those can diverge once Task Scheduler or a service account changes %TEMP%, the working directory, or a mounted drive mapping.
Check each unique filesystem root on this host and fail if any exceeds the configured usage threshold.
These scripts solve the glue problems around job scheduling: preventing overlapping runs, retrying flaky commands, and alerting on outcomes. They complement orchestrators like airflow-dag-patterns by handling concerns that Task Scheduler and cron do not address natively.
Orchestration scripts
Mutex lock wrapper
When a job moves from one-off execution into unattended scheduling. It is typically triggered when the scheduler needs extra control over overlap, retries, or notifications. Prevents overlapping executions of the same job by acquiring a system-wide named mutex before running the command. Without this, a scheduled task that takes longer than its interval will spawn a second instance, leading to duplicate data, race conditions, or resource exhaustion.
A named mutex is host-local coordination
It prevents overlap on the same Windows host. It does not coordinate multiple runners, containers, or VMs. Once the same job can run on more than one machine, move the lock into a shared service.
Acquire a named mutex before running the helper script under data/powershell-automation/state.
When a job moves from one-off execution into unattended scheduling. It is typically triggered when the scheduler needs extra control over overlap, retries, or notifications. Wraps any command with configurable retry count and exponential backoff. This is a reusable building block for any operation that may fail transiently — database connections, API calls, file transfers. The backoff prevents hammering a recovering service.
Retry logic needs replay safety
The wrapper is sound for transient read failures. It becomes unsafe when copied onto writes that can be applied more than once.
Retry a non-idempotent write
A timeout or dropped connection after the remote side commits can still produce a duplicate write on the next attempt.
while ($attempt -lt $MaxRetries) { Invoke-RestMethod -Uri $Url -Method Post -Body $payload}
Retry an idempotent check
Keep the generic wrapper around health checks, metadata reads, or writes protected by an idempotency key.
while ($attempt -lt $MaxRetries) { Invoke-Sqlcmd -ServerInstance 'localhost,1434' -Database 'stoxx' -Username 'sa' -Password 'EsgDev2026Pass1' -TrustServerCertificate -Query 'SELECT 1 AS HealthCheck;'}
Retry a transiently failing operation until the third attempt, then complete with a live stoxx health query.
Attempt 1/3 failed, retrying in 1s...Attempt 2/3 failed, retrying in 2s...OK - succeeded on attempt 3
Run and alert pattern
When a job moves from one-off execution into unattended scheduling. It is typically triggered when the scheduler needs extra control over overlap, retries, or notifications. Executes a command and sends a notification to a Slack webhook (or any HTTP endpoint) with the outcome — success or failure. This is the simplest possible alerting layer for scheduled tasks that run unattended. Without it, a nightly job can fail silently for days before anyone notices.
Alert transport is secondary to job truth
A webhook timeout should not flip a successful data job into a failed one, and a delivered notification should not hide a failed primary command. Keep the command exit code authoritative and handle notification errors on a separate path.
Run the stoxx health helper, post the outcome to a local webhook listener, and emit a scheduler-friendly status line.
Most PowerShell automation failures come from shell semantics and runtime context rather than from CSV or JSON handling itself. These patterns keep the scripts above predictable when they move from an interactive terminal into unattended production jobs.
Script defaults
Fail fast on PowerShell errors
Set $ErrorActionPreference = 'Stop' and Set-StrictMode -Version Latest near the top of automation scripts so provider and cmdlet failures become terminating control-flow decisions instead of console noise. This does not change native executable behavior, but it does stop the script before later steps consume partial state.
Run a child PowerShell script that aborts on a missing file and surfaces the resulting non-zero exit code.
beforeCannot find path 'C:\Users\aperi\My Drive\VAULT\data\powershell-automation\state\missing-demo-file.txt' because it does not exist.Exit code: 1
Treat native exit codes as a separate channel
After python.exe, sqlcmd.exe, bcp.exe, gcloud, bq, or any other native executable, test $LASTEXITCODE explicitly and throw on non-zero values. $ErrorActionPreference does not cover native process failures because PowerShell did not create the error record.
Show that a failing native process does not enter catch until the script converts $LASTEXITCODE into an exception.
After native callLASTEXITCODE=7Caught: Native exit 7
Cleanup and scheduled execution
Keep cleanup in finally
Use try / catch / finally whenever the script creates temp files, acquires locks, opens connections, or writes partially complete artifacts. Cleanup belongs in finally so failure paths do not leak state.
Create a temp file under state, fail intentionally, and confirm that finally removes the file on exit.
Temp file createdSimulated failureCleanup exists after finally: False
Make Task Scheduler context explicit
Assume Task Scheduler is a different runtime than your shell session. Set the working directory, environment variables, execution policy, and service identity explicitly, and prefer “Run whether user is logged on or not” for unattended production jobs.
Compare a minimal scheduled-task-like launch with a run that sets the working directory and required environment explicitly.
A lightweight helper is enough when you need searchable timestamps in flat-file automation and do not yet have centralized logging. Keep the timestamp format fixed so downstream parsers and operators can sort lines lexically.
Emit two timestamped log lines with a small delay between them.
Prefer Invoke-Sqlcmd for structured SQL Server output
Install the SqlServer module when the next step expects objects or CSV export instead of console-formatted text. Invoke-Sqlcmd keeps query results in the same object pipeline as the rest of the PowerShell automation.
Return one live row from stoxx and show the object type and business columns that remain inside the pipeline.
Use these symptoms to decide whether the failure is scheduler context, PowerShell error handling, native process handling, or input parsing.
Scheduling context
Script works interactively but fails in Task Scheduler
Check the scheduled task’s working directory, execution policy, user identity, and environment variables first. The command often succeeds manually because the interactive shell has profile state and credentials that the scheduled task does not inherit.
Read the same relative-path script once from its expected folder and once from C:\Windows\System32 to simulate a scheduler launch without Start in.
Interactive-style run:relative file availableScheduler-style run:Cannot find path 'C:\Windows\System32\scheduler-relative-input.txt' because it does not exist.Exit code: 1
”Running scripts is disabled”
The effective execution policy for the scheduled user is still too restrictive. Set RemoteSigned for the intended scope or launch PowerShell with -ExecutionPolicy Bypass when policy exceptions are part of the run model.
Launch a temporary script under Restricted to reproduce the policy failure that Task Scheduler surfaces.
running scripts is disabled on this system. For more information, see about_Execution_Policies atExit code: 1
Error handling
catch never executes
The failing command is still producing a non-terminating PowerShell error. Set $ErrorActionPreference = 'Stop' before the try block or add -ErrorAction Stop to the cmdlet that is expected to fail.
Compare the same missing-path cmdlet without and with -ErrorAction Stop.
Without Stop:Cannot find path 'C:\Users\aperi\My Drive\VAULT\data\powershell-automation\state\missing-catch-demo.txt' because it does not exist.after try blockWith -ErrorAction Stop:caught terminating error
Native executable failure is not caught
A native process returned a non-zero exit code, but PowerShell did not throw an exception. Inspect $LASTEXITCODE immediately after the native call and convert non-zero values into an explicit throw or exit.
Run the same failing native command once without and once with explicit $LASTEXITCODE handling.
Without explicit LASTEXITCODE handling:catch not enteredLASTEXITCODE after native call: 5With explicit LASTEXITCODE handling:Native exit 5
Data parsing
Import-Csv returns the wrong columns
The file delimiter does not match the parser expectation. Pass the correct delimiter explicitly, such as Import-Csv -Delimiter ';', Import-Csv -Delimiter "t”, or Import-Csv -UseCulture`, and verify the upstream extract format before debugging the downstream logic.
Parse a semicolon-delimited file first with the default parser and then with the correct delimiter.