The kaab CLI
Run jobs, push and lint programs, review proposals and manage schedules and webhooks from a terminal.
kaab is a single binary for Linux, macOS and Windows, built on the Go SDK. It runs jobs, keeps
programs in files you can version and review, and answers parked jobs, so a terminal or a CI job
can do what the panel does.
#When to use it
Use the CLI from a shell, a CI pipeline or a cron job on your own machine: to run a program and save its data, to lint and push program files from a repository, or to approve proposals in bulk. From application code, use an SDK instead; for building and testing a program interactively, the program builder is quicker.
#Install
With Go 1.24 or later:
go install github.com/kaabist/kaab-go/cmd/kaab@latest
kaab version
Each release is also built as archives for Linux, macOS (darwin) and Windows, on amd64 and
arm64: kaab_<version>_<os>_<arch>.tar.gz (.zip on Windows), each with the binary, its licence
and README, and a checksums.txt to verify them against. Unpack one and put kaab on your PATH.
There is no Homebrew or winget package yet.
Shell completion comes with the binary:
kaab completion zsh > "${fpath[1]}/_kaab" # also bash, fish, powershell
#Sign in
kaab auth login # paste an sk_live_ or sk_test_ key
kaab auth status
The key is checked against the API and kept in your operating system's keychain (or, where there
is none, in ~/.config/kaab/credentials.toml, readable only by you). A profile holds one live and
one test key; --test uses the test one, --profile picks another profile. KAAB_API_KEY and
KAAB_BASE_URL override any profile, which is what CI should use.
#Run a job
response=$(curl -sS --fail-with-body "${KAAB_BASE_URL:-https://api.kaab.ist/v1}/jobs" \
-H "Authorization: Bearer $KAAB_API_KEY" \
-H "Content-Type: application/json" \
-H "Prefer: wait=30" \
-d '{
"url": "'"${SITE_URL:-https://shop.example.com}"'/p/1",
"extract": {
"title": "h1",
"price": { "selector": ".price", "type": "money", "locale": "tr", "currency": "TRY" }
}
}')
echo "$response"
// One call: create the job and hold the request up to 30 s for its result (`Prefer: wait`).
const job = await kaab.jobs.create({
url: `${site}/p/1`,
extract: {
title: "h1",
price: { selector: ".price", type: "money", locale: "tr", currency: "TRY" },
},
wait: 30,
});
if (job.status !== "succeeded") throw new Error(`job ${job.id} is ${job.status}`);
console.log(`${job.id}: ${JSON.stringify(job.data)}`);
# One call: create the job and hold the request up to 30 s for its result (Prefer: wait).
job = kaab.jobs.create(
url=f"{site}/p/1",
extract={"title": "h1", "price": {"selector": ".price", "type": "money", "locale": "tr", "currency": "TRY"}},
wait=30,
)
if job.status != "succeeded":
raise SystemExit(f"job {job.id} is {job.status}")
print(f"{job.id}: {job.data.to_dict() if job.data else None}")
// One call: create the job and hold the request up to 30 s for its result (Prefer: wait).
body, err := kaab.Body[client.CreateJobJSONBody](map[string]any{
"url": site + "/p/1",
"extract": map[string]any{
"title": "h1",
"price": map[string]any{"selector": ".price", "type": "money", "locale": "tr", "currency": "TRY"},
},
})
if err != nil {
log.Fatal(err)
}
job, err := c.Jobs.Create(ctx, body, kaab.WithWait(30))
if err != nil {
log.Fatal(err)
}
if job.Status != "succeeded" {
log.Fatalf("job %s is %s", job.ID, job.Status)
}
fmt.Printf("%s: %v\n", job.ID, *job.Data)
data=$(kaab jobs run \
--url "${SITE_URL:-https://shop.example.com}/p/1" \
--extract '{"title": "h1", "price": {"selector": ".price", "type": "money", "locale": "tr", "currency": "TRY"}}' \
--wait 30)
echo "$data"
More forms of the same command:
kaab jobs run --url https://shop.example.com/p/1 --extract @schema.json --wait 30
kaab jobs run --program acme-report@production --input report_id=42 --session acme --wait 60
A succeeded job prints its data as JSON on stdout and a one-line summary on stderr, so
kaab jobs run … > data.json does what it says. --json prints the whole envelope instead.
| Exit code | Meaning |
|---|---|
| 0 | succeeded, or still running when --wait ran out |
| 1 | the request failed (the API's code, message and request id are printed) |
| 2 | the job failed or was canceled |
| 3 | the job is parked: waiting for an input (kaab jobs input) or for attention (kaab jobs resume, kaab open job) |
#Programs, proposals, schedules, webhooks
kaab programs lint report.json # exit 1 on errors
kaab programs push report.json --publish
kaab programs pull acme-report@production -o report.json
kaab programs diff acme-report 3 4
kaab proposals list && kaab proposals approve prp_… --promote
kaab schedules create --program acme-report@production --cron "0 * * * *" --change-detection --field price
kaab webhooks create --url https://example.com/hooks --events 'job.*'
kaab credits && kaab usage --group-by program
kaab <command> --help describes every flag, and the CLI reference lists every
command.
#Worked example
Run a shop's product page, https://shop.example.com/p/1, from a terminal and keep only the price:
export KAAB_API_KEY=sk_test_…
cat > schema.json <<'JSON'
{ "title": "h1", "price": { "selector": ".price", "type": "money", "locale": "tr", "currency": "TRY" } }
JSON
kaab jobs run --url https://shop.example.com/p/1 --extract @schema.json --wait 30 > data.json
echo $? # 0
data.json holds the job's data: the title Kulaklık Klasik 101 and the price
{ "amount": 1337.4, "currency": "TRY" }.
In a CI step, branch on the exit code: 2 means the job failed, 3 that it parked and somebody has
to answer it.
#Pitfalls
- A live key in CI by accident.
KAAB_API_KEYwins over every profile; give CI ansk_test_key for checks and a narrowly scoped live key only where it does real work. - Treating exit code 0 as "done". It also means the job was still running when
--waitran out. Use--jsonand readstatuswhen the difference matters. - Forgetting
--publish.kaab programs pushwithout it saves a draft version that no alias runs. - Shell globbing in
--events 'job.*'. Quote event patterns.