Exit Codes
tabstack uses four exit codes, each with a distinct meaning. The distinction matters more than it might seem.
The scheme
| Code | Meaning | What to do |
|---|---|---|
0 | Success | Continue |
1 | Runtime / API error | Investigate; may be transient |
2 | Usage error | Fix the command |
3 | Task reported failure | The automation concluded it failed |
Code 0 — success
The command completed and produced output. For streaming commands, this means the final event arrived and the stream closed cleanly.
tabstack extract markdown https://example.com
echo $? # 0 Code 1 — runtime error
Something went wrong that isn’t your fault as the caller. Network failures, API 4xx/5xx responses, timeout, JSON parse errors from the server. These are transient or environmental — worth retrying, investigating, or alerting on.
tabstack extract markdown https://example.com
# Error: API returned 503 — service temporarily unavailable
echo $? # 1 In a CI pipeline: exit 1 from a tool you depend on is the signal to retry or fail the build with an investigation note.
Code 2 — usage error
You passed bad arguments. Unknown flags, missing required values, invalid schema JSON, unrecognized subcommands. The problem is in the command line, not the runtime.
tabstack extract markdown # missing URL
# Error: extract markdown requires a URL
echo $? # 2
tabstack --fetch-timeout abc extract markdown https://example.com
# Error: --fetch-timeout must be a number, got "abc"
echo $? # 2 Why distinguish 2 from 1? Because the handling is completely different. A code 2 error will reproduce every time regardless of retries — the command is wrong. A code 1 error might succeed on retry. Treating them the same means either retrying hopeless commands or failing immediately on transient errors. Scripts and CI pipelines that check $? can branch correctly only if the codes carry this distinction.
Code 3 — task failure
This one is specific to automate. The automation ran to completion but the agent concluded it failed — “I couldn’t find the element”, “the form didn’t submit”, “login was rejected”. The task was attempted and reported failure.
tabstack automate "submit the contact form" --url https://broken.example.com
# Task completed — agent reported: could not locate the submit button
echo $? # 3 Code 3 is not a crash. The stream closed cleanly. The agent returned a task:failed event with a reason. This is distinct from code 1 (which would mean the API itself failed, not the task).
Why does this need its own code? Because an agent running tabstack as a subprocess needs to distinguish “the API failed, check connectivity” (code 1) from “the task couldn’t be done, rethink the instructions” (code 3). Without the distinction, both look like $? != 0 and the agent can’t know whether to retry, escalate, or change approach.
NDJSON and code 3
When piped, streaming commands emit NDJSON. A task failure arrives as an event before the process exits:
tabstack automate "fill the form" --url https://example.com
| jq 'select(.event=="task:failed")'
# {"event":"task:failed","data":{"reason":"Could not locate submit button"}} The exit code and the NDJSON event carry the same signal through two channels — one for shells, one for JSON parsers. Check both when building reliable pipelines.
In scripts
tabstack extract json "$URL" --schema @schema.json > result.json
STATUS=$?
if [ $STATUS -eq 2 ]; then
echo "Bad command — check your schema or URL" >&2
exit 1
elif [ $STATUS -eq 1 ]; then
echo "API error — retrying in 30s" >&2
sleep 30 && retry_logic
elif [ $STATUS -eq 3 ]; then
echo "Task failed — check result.json for details" >&2
fi