JSON output
The envelope/1 document every command prints with --json, field by field, and how to parse it.
Every Kalup command takes --json. With it, the command prints exactly one JSON document, envelope/1, to stdout and nothing else. Scripts, CI jobs and AI agents should always use it: the shape is versioned and fixed, while the human text may change.
The envelope
{
"format": "envelope/1",
"ok": true,
"data": { "valid": true, "counts": { "errors": 0, "warnings": 0 } },
"issues": []
}| Field | Type | Always present | Meaning |
|---|---|---|---|
format | "envelope/1" | Yes | The version of this shape. It changes only additively inside envelope/1. |
ok | boolean | Yes | true when the command succeeded: exit 0, or exit 2 (differences found with --exit-code). |
data | object | No | The command's result. Absent when the command failed before it had one. See What data holds. |
issues | Issue[] | Yes | Errors and warnings, possibly empty. On success, only warnings. |
Issues
Every error and warning is one issue:
{
"code": "E_UNKNOWN_TARGET",
"message": "target 'staging' is not declared",
"file": "kalup.config.ts",
"line": 9,
"configPath": "targets",
"fix": "use one of sandbox, production, or declare targets.staging",
"docs": "errors/E_UNKNOWN_TARGET.md"
}| Field | Type | Always present | Meaning |
|---|---|---|---|
code | string | Yes | A stable code. E_ is an error, W_ a warning. Every code is listed in Errors. |
message | string | Yes | One sentence saying what is wrong. May change between versions; do not match on it. |
file | string | No | The file, relative to the project root, when the issue is in one. |
line | number | No | The 1-based line in file. |
configPath | string | No | Where in the config the issue is, as a dotted path: Company.properties.seatCount.fieldType, targets.sandbox.portalId. |
fix | string | No | What to do about it, in one line. |
docs | string | No | The page for this code inside the installed package, relative to its docs/ folder. The same text is on Errors. |
humanRequired | boolean | No | true when only a person can resolve it, for example a key for the wrong portal. Such an issue comes with exit 4. |
Warnings ride in issues even when ok is true. Tell them apart by the W_ prefix on code.
Where human text goes
Without --json | With --json | |
|---|---|---|
| stdout | The command's report, such as Config valid (0 errors, 0 warnings) or the IR | One envelope/1 document |
| stderr | One line per issue: file:line: CODE: message (fix: ...) (docs: ...) | Nothing from Kalup |
--help and --version follow the same rule: with --json they are envelopes too, with data.usage and data.name, data.version, data.disclaimer.
What data holds
| Command | data | Shape |
|---|---|---|
init | The new project | target, portalId, account, objects, scopes, files, and pull with the first pull's result (absent when that pull failed) |
pull | What was read and written | target, portalId, objects (a report per object in scope), files |
pull --discover | What the scope leaves out | target, portalId, objects (custom objects not in config), properties (per object, portal properties not in scope) |
validate | The verdict | valid, counts.errors, counts.warnings |
ir | The IR document | irVersion, project, generator, resources, targets, tombstones. Absent with --check. |
fmt | The files changed | changed: paths rewritten, or with --check, paths that would be |
status | Config and targets | config (validity and counts), targets (one entry per target with its checks) |
Each command page documents its data in full.
When ok is false, data may still be present. validate always returns its verdict, status returns every target's checks even when one of them failed, and init keeps its result when only the first pull failed. ir, fmt and pull omit data on failure.
Parsing it robustly
- Read stdout as a whole and parse it as one JSON document. Do not read it line by line.
- Check
format === "envelope/1"before you trust the rest. - Branch on the process exit code first, then on
ok. The exit code tells you what kind of outcome it is;okalone cannot tell "invalid config" from "a person is needed". - Match issues on
code, never onmessage. - Ignore fields you do not know. New optional fields may appear inside
envelope/1.
A small example in Node:
import { execFileSync } from 'node:child_process'
function kalup(args: string[]) {
try {
const stdout = execFileSync('npx', ['kalup', ...args, '--json'], { encoding: 'utf8' })
return { exitCode: 0, envelope: JSON.parse(stdout) }
} catch (error) {
// a non-zero exit still printed an envelope on stdout
const e = error as { status: number; stdout: string }
return { exitCode: e.status, envelope: JSON.parse(e.stdout) }
}
}
const { exitCode, envelope } = kalup(['validate'])
if (exitCode === 3) {
for (const issue of envelope.issues) console.log(`${issue.file}:${issue.line} ${issue.code} ${issue.fix ?? ''}`)
}In a shell, with jq:
npx kalup validate --json | jq -r '.issues[] | select(.code | startswith("E_")) | "\(.file):\(.line) \(.code)"'What it never contains
No envelope carries a key or a token, including in error messages: only the name of the variable a key is read from. Text that came from a portal is stripped of control characters and newlines, and capped in length, before it is printed. See Network and safety.