Exit Codes

tabstack uses four exit codes, each with a distinct meaning. The distinction matters more than it might seem.

The scheme

CodeMeaningWhat to do
0SuccessContinue
1Runtime / API errorInvestigate; may be transient
2Usage errorFix the command
3Task reported failureThe 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