Exit Codes and Error Handling
What each exit code means, why a success message is not proof, and how to make a script fail loudly.
A script that carries on after a failed step produces a model that looks built and is not. This article is about making that impossible.
The exit codes#
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Parse failure, a verb not available in this edition, or a model, template or deployment verb that refused or failed |
| 5 | A database-level verb completed with a warning |
| 10 | A database-level verb failed |
Exit 10 covers the database-level verbs: --create, --migrate, --backup, --import, and --license. It means the operation failed — not specifically a migration problem, which is a reasonable but wrong assumption to make from the number.
Exit 5 is the same family's warning code. --migrate returns it when the migration ran but the instance's version property did not change, and --license returns it when the key was saved but could not be validated or its maintenance has already expired. Treat it as a failure in a pipeline: the instance is not in the state you asked for.
Exit 1 covers a malformed command line and the verbs gated out of Flow 7.2.0, both caught before anything is changed. It is also the code every other verb uses when it refuses or fails — a missing --entity-id, an entity that does not exist, a precondition the verb checks (an unreleased template, a measure on several calendars with no calendar named, a Flow Bootstrap that rejected a registration). The message on stderr, and in the log, says which.
Check the exit code, always#
The single most important rule when scripting Infohub:
& $exe @conn --create --name "Demo Plant"
if ($LASTEXITCODE -ne 0) { throw "create failed with exit code $LASTEXITCODE" }
In bash:
"Flow Config.exe" ... --create --name "Demo Plant" || exit 1
Or make the whole script fail fast:
set -euo pipefail
A success message on stdout is not proof of success. Read the exit code, not the output. Success lines are gated on the exit code in 7.2.0, so the two now agree — but the habit of trusting printed text rather than the code is what makes scripts fragile across versions, and it is the habit worth breaking regardless.
Why --create deserves special attention#
--create lays the schema down as a base version and then upgrades it. If it throws part-way — an unreachable host, bad credentials, an assembly that will not load — the result is a partially created database: some of the tables, not all of them.
In 7.2.0 that failure correctly exits 10. A script that checks the exit code stops there and reports a real error.
A script that does not check it proceeds to build a model against a half-built schema, and every subsequent step fails in confusing ways that look like model problems rather than a failed create.
Verify the create, not just its exit code. For anything important, confirm the instance is usable before building on it:
& $exe @conn --create --name "Demo Plant"
if ($LASTEXITCODE -ne 0) { throw "create failed ($LASTEXITCODE)" }
# prove it responds
& $exe @conn --list-connection-types --output-format json > $null
if ($LASTEXITCODE -ne 0) { throw "instance created but not usable" }
Assert the outcome, not just the absence of failure#
Exit codes catch failures the CLI knows about. They cannot catch a command that succeeded and did less than you intended.
The clearest example is template instantiation. An instantiation that creates zero entities still exits 0, with a warning — it is how the CLI reports that matching instances already exist in the destination. (Instantiating a template that has not been released used to produce the same silent zero; it is now refused with exit 1 and a message naming the --release command to run.) A script that checks only the exit code proceeds to build charts against measures that do not exist.
So for operations where "did nothing" is a plausible outcome, assert the result:
# instantiate three units
& $exe @conn --instantiate --source-entity-type Metric --source-entity-id $tpl --destination-entity-type MetricFolder --destination-entity-id $dest --naming-pattern "Unit {0}" --instance-count 3
if ($LASTEXITCODE -ne 0) { throw "instantiate failed" }
# assert three arrived
$metrics = & $exe @conn --list-metrics --metric-folder-id $dest --output-format json | ConvertFrom-Json
if (@($metrics | Where-Object { $_.Name -like 'Unit *' }).Count -ne 3) { throw "expected 3 instances in folder $dest" }
Operations worth asserting after:
| Operation | Assert |
|---|---|
--instantiate |
The expected number of entities exist |
--create-chart plus measure adds |
--list-chart-tree shows the series |
--import-model |
The entities arrived, and resolved to the right connections |
--deploy |
--deployment-status shows the component as deployed |
| Any bulk create | The count matches what you asked for |
--delete-entity |
The entity is gone from the matching --list-* verb |
This is the difference between a script that reports success and a script that produces a working model.
Capture the log#
Use --logfile on every step in an automated run, and keep the logs as build artefacts. When a pipeline fails at step 340, the log is what tells you why.
& $exe @conn --logfile "$logDir\step-$n.log" --create-measure ...
In 7.2.0 the log is written even for parse errors and gated verbs, so a failing step always leaves evidence. On earlier builds it did not, which is worth knowing if you maintain scripts targeting an older Infohub.
Write one log per step, or one per run with a step marker — either is fine, as long as a failure is attributable.
Continue or stop?#
Two defensible strategies, and the choice should be deliberate.
Stop on first failure. Correct for a promotion pipeline, where a partially applied change is worse than no change.
Continue and report. Correct for a large model build, where one failed measure out of four hundred should not discard the run — provided the script keeps a tally and fails at the end if anything failed.
Infohub's own demo-model build scripts take the second approach: each step logs pass or gap, the run continues, and the summary reports what did not work. That is the right shape for building, because it surfaces all the problems in one run rather than one per run.
What is never acceptable is continuing silently. If a step failed and the run exits 0 with no summary, the script is lying about its outcome.
Distinguishing the failure classes#
When a step fails, the code tells you which kind of problem you have:
| Code | Look at |
|---|---|
| 1, before anything ran | The command line itself — a typo, a wrong option name, or a gated verb |
| 1, from a verb | The message — a missing argument, an ID that does not exist, or a precondition the verb refused on |
| 5 | The warning — the migration or licence step did not leave the instance where you expected |
| 10 | The database or the environment — connectivity, credentials, permissions, disk |
| 0 but wrong result | The logic — instances that already existed, or a chart with no series |
The last row is the one that consumes afternoons, and asserting outcomes is how you convert it into an immediate, clear failure.
Several verbs that once reported success and did nothing now refuse instead, which moves them from the last row to the second:
| Verb | Now refused with exit 1 when |
|---|---|
--instantiate, --derive |
The source template is not released (locally, or on the publishing instance for a template-server source) |
--update-from-template, --update-all-out-of-sync |
The entity's template is not released; a failed update also now prints the import's reason |
--register-with-bootstrap |
Flow Bootstrap answered but rejected the registration — bad credentials, an unreachable instance database, an instance with no identity — and the response body is printed |
--attach-measure-consumer |
The instance has no Integration Engine, or the measure sits on more than one calendar or event scheme and neither --time-scheme-id nor --event-scheme-id says which |
--attach-measure-event-scheme |
The measure aggregates a different event scheme |
--create-metric-folder, --create-template-folder |
The parent's type cannot hold the folder (a template metric folder at root, for example) |
Idempotency and re-runs#
A script that fails halfway will be re-run. If re-running produces duplicates, every failure becomes a manual cleanup.
A few verbs are idempotent by design — the lookup-row creates --create-chart-component-type and --create-chart-item-type, and --attach-measure-event-scheme, which re-writes the event scheme sets on a measure that already carries the scheme rather than adding a second link. Everywhere else, check before creating. See Scripting Patterns and Idempotency.
Several verbs also take --dry-run and report what they would change without changing it: --delete-entity, --gap-skip-to, --import-model, --update-all-out-of-sync, --deploy and --undeploy. A dry run is the cheapest rehearsal of a step you cannot easily undo.
A robust step wrapper#
Worth writing once and reusing:
function Invoke-FlowStep {
param([string]$Description, [string[]]$Arguments)
$script:stepNo++
$log = Join-Path $logDir ("{0:d4}.log" -f $script:stepNo)
& $exe @conn --logfile $log @Arguments | Out-Null
if ($LASTEXITCODE -ne 0) {
$script:failures += "[$script:stepNo] $Description -> exit $LASTEXITCODE (see $log)"
Write-Host " FAIL $Description (exit $LASTEXITCODE)"
return $false
}
Write-Host " ok $Description"
return $true
}
With a summary at the end that sets the script's own exit code:
if ($failures.Count -gt 0) {
Write-Host "`n$($failures.Count) step(s) failed:"
$failures | ForEach-Object { Write-Host " $_" }
exit 1
}
Write-Host "`nAll $stepNo steps succeeded."
Every step logged, every failure attributable, and the script's own exit code honest about the outcome. That last part matters most — a build script that exits 0 after failures is worse than one that never ran.
Checklist for a script you will rely on#
- Check
$LASTEXITCODEafter every invocation. - Assert the outcome of anything where "did nothing" is possible.
- Use
--logfileon every step and keep the logs. - Take credentials from the environment, never hard-coded.
- Decide stop-on-failure or continue-and-report, explicitly.
- Make the script's own exit code reflect whether anything failed.
- Verify the instance is usable after
--createbefore building on it.