@kizenapps/cli
v1.19.0
Published
CLI tool for running Kizen plugin apps locally
Readme
@kizenapps/cli
A local development environment for Kizen plugin apps.
appbuilder scaffolds a new plugin, bundles it, and runs a live viewer in a dedicated Chromium window so you can iterate on your plugin against any Kizen environment without having to publish, deploy, or reload by hand.
Requirements
| Requirement | Version | Why |
| ------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Node.js | >=20 | Enforced by engines in package.json. |
| Google Chrome or Chromium | any recent release | The viewer is launched via chrome-launcher against a locally installed browser. No browser is bundled. |
| Python | 3.12 or newer | Only needed to run code steps locally and for appbuilder admin. For code steps, the dev server builds a virtualenv with the interpreter the step's runtime asks for (python-3-12 / python-3-13), matching the runtime images the hosted Kizen code-runner ships. appbuilder admin uses the newest Python 3.12 or newer it finds. |
chrome-launcher auto-discovers an installed Chrome/Chromium; if yours lives somewhere unusual, set CHROME_PATH to the executable and it will be preferred.
Python is resolved lazily — the CLI only looks for an interpreter the first time a plugin actually runs a code step, or the first time you run appbuilder admin, so you can build UI-only plugins without it.
Installation
Run it without installing:
npx @kizenapps/cli devOr install it globally. The published binary is named appbuilder:
npm install -g @kizenapps/cli
appbuilder devEvery commit to main publishes a prerelease under the next dist-tag (versioned <version>-<short-sha>); tagged releases go to latest. To pick up an unreleased fix:
npm install -g @kizenapps/cli@nextQuickstart
1. Scaffold a plugin
appbuilder createThe wizard first asks where the plugin should live (the current directory, or a new sub-directory named after the API name), then collects five fields:
| Field | Required | Notes |
| ------------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| Name | yes | Human-readable plugin name. |
| API name | yes | Defaults to a snake_cased version of the name. Hyphens are rejected by the platform. |
| External link | no | Documentation or marketing URL for the plugin. |
| Description | no* | See the note below. |
| Business ID | no* | Prefilled from your appbuilder login session's business when you're signed in, else from your stored API-key credentials. |
Fill in Description and Business ID. Both are labelled optional in the wizard, but
createwrites them intokizen.jsonas empty strings and the bundler rejects an emptydescriptionordeveloper_business_id— so a plugin created with those fields skipped failsappbuilder builduntil you editkizen.jsonby hand. This is tracked internally (KZN-17594); until that fix lands, treat both as required.
It then asks which artifacts to scaffold — Floating frame, Block, Data adornment, Routable page, Toolbar item, Object settings item, JS action — with all seven selected by default (Space toggles, a selects all, n none). Each selected type gets a working hello* directory under src/, holding a config.json and a script that runs as-is.
create writes kizen.json, src/, releaseNotes/, a placeholder src/thumbnail.png (512×512, colored from the api_name), the templates for the artifacts you picked, and the Copilot review files described under appbuilder create. It adds both .kizenapp/ and .copilot-docs/ to .gitignore.
2. Set up credentials
API-key credentials are deprecated for block commands.
appbuilder block pushandappbuilder block targetsuse yourappbuilder loginsession by default.appbuilder devandappbuilder encryptstill use API keys:encryptuntil Plugin Wizard accepts sign-in sessions, and OAuth fordevis planned. The rest of this section applies to them.
appbuilder dev talks to a real Kizen environment on your behalf, so it needs four things:
| Field | Where it comes from |
| ----------- | --------------------------------------------------------------------------------------------------- |
| API Key | An API key issued for your Kizen user, from the Kizen app. |
| User ID | The UUID of your Kizen user. |
| Business ID | The UUID of the Kizen business you are developing against. |
| Environment | One of go, fmo, staging, integration, test1 — which Kizen deployment the above belong to. |
They are sent as X-API-KEY / X-USER-ID / X-BUSINESS-ID headers on every proxied request, so they must all belong to the same environment.
On the first appbuilder dev in a plugin directory you are prompted to pick a credential mode:
- Global — credentials are written to
~/.kizenappbuilder/credentials.json(directory0700, file0600) and shared across every plugin on the machine. You can keep additional named profiles alongside it as~/.kizenappbuilder/<profile>.jsonand switch between them withcin the TUI. - Local — nothing is written to disk by the CLI; you enter credentials inside the viewer, and they live in the browser profile under
.kizenapp/.
The choice and the active profile name are remembered in .kizenapp/config.json, so subsequent runs load silently. --credentials <path> bypasses all of this and reads a specific JSON file.
3. Run the dev server
cd my-plugin
appbuilder devThis builds the plugin, starts the local server on port 3121, and opens the viewer in a dedicated Chromium window. Every file change rebuilds and hot-reloads; if validation fails, the error appears in the TUI and the viewer keeps the last good bundle.
TUI keys: v launches the viewer (useful with --no-viewer), c switches credential profile, q quits.
4. Produce a bundle
appbuilder buildWrites .kizenapp/bundle.json — the same artifact dev serves — after running the full validation pass. Use this in CI or whenever you want a bundle without starting a server.
Commands
appbuilder create
Scaffolds a new Kizen plugin project. Interactive by default; passing any flag other than --include-lib switches it to a non-interactive run, including --artifacts to choose the artifact templates up front (appbuilder create --help lists them all). Pass --include-lib to also write the Kizen data helper library to src/lib/kizenData.js; without it, no library is written. --business-id and --environment default from your appbuilder login session: the environment is -e, else the only environment you're signed in to, else go, and the business is that environment's session business. Only when you're signed out entirely does it fall back to ~/.kizenappbuilder/credentials.json. See Quickstart for the fields it collects, the artifact picker, and everything it writes.
The scaffold also includes .github/copilot-instructions.md, two path-scoped instruction files under .github/instructions/, and .github/workflows/copilot-code-review.yml. That workflow fetches the Kizen plugin docs from kizen/app-engine into .copilot-docs/ before Copilot code review runs, so reviews cite the current documentation instead of rules copied into the plugin repo. It only takes effect once it is on the repository's default branch, and .copilot-docs/ is gitignored. Plugins scaffolded before this setup existed can pick it up with appbuilder setup-copilot.
It also writes the coding-agent skills, once for Claude Code (.claude/skills/) and once for Codex (.agents/skills/): kizen-custom-block (building custom blocks, with its design guide) and kizen-business-apps (publishing with appbuilder app). Plugins scaffolded before a skill existed pick it up with appbuilder setup-claude or appbuilder setup-codex.
appbuilder setup-copilot
Writes the same Copilot review files into an existing plugin repo — run it from the repo root (a kizen.json must be there). The four files are CLI-owned, so the command overwrites them and prints created, updated, or unchanged per file; a clobbered local customization is recoverable from git. A file (or a folder on its path) that is a symbolic link is refused, and nothing is written. It also adds .kizenapp/ and .copilot-docs/ to .gitignore if they are missing.
| Flag | Default | Purpose |
| ----------- | ------- | ----------------------------------------------- |
| --dry-run | off | Print the per-file statuses without writing any |
As with create, the workflow only runs once it has reached the repository's default branch.
appbuilder setup-claude
Installs or refreshes the Claude Code skills in the plugin in the current directory: kizen-custom-block for building custom blocks (.claude/skills/kizen-custom-block/SKILL.md) and kizen-business-apps for publishing with appbuilder app (.claude/skills/kizen-business-apps/SKILL.md). appbuilder create writes the skills once; run setup-claude in an existing plugin to add them, or after upgrading the CLI to pick up the bundled versions. With --mcp it only registers the Kizen MCP server instead and writes no files, so it can run in any directory (see below).
appbuilder setup-claude [--dry-run] [--include-lib]
appbuilder setup-claude --mcp [--env <env>] [--force] [--dry-run]| Flag | Default | Purpose |
| --------------- | ------- | -------------------------------------------------------------------------------------------------------------- |
| --dry-run | off | Report what would change without writing files. |
| --include-lib | off | Also install the Kizen data helper library (kizenData.js). |
| --mcp | off | Only register the Kizen MCP server in Claude Code (user scope); writes no files, so it works in any directory. |
| --env <env> | go | Environment of the MCP server to register. |
| --force | off | Replace an existing registration of that server. |
--env and --force only apply with --mcp, and --mcp can't be combined with --include-lib; see Using the session from Claude Code.
It also installs the skill's design guide (.claude/skills/kizen-custom-block/design.md), which tells the agent how to make blocks match native dashlets. With --include-lib, it installs the Kizen data helper library that block scripts import to read records and apply the dashboard's date and team filters. The library goes under the entry directory from kizen.json (<entry>/lib/kizenData.js, one per entry in a multi-plugin kizen.json), and under src/ when kizen.json has no usable entry. Without the flag, it refreshes only the libraries that already exist. appbuilder create --include-lib writes it to src/lib/kizenData.js.
It prints one line per file (created, updated or unchanged) and a one-line summary. These files are managed by the CLI, so local edits to them, including edits to kizenData.js, are replaced when they're updated. Don't edit kizenData.js; put your own helpers in another file under src/lib/. It only writes these files and never touches the .github/ Copilot files, the Codex skills or block code. It writes only regular files inside the plugin folder: if a managed file, or any folder on its path, is a symbolic link, or something other than a file or folder is in the way, it writes nothing and fails with exit code 1, naming the path. That includes the entry directory: with --include-lib (or an existing kizenData.js), an entry directory such as src that is a symbolic link, as some monorepo setups use, is refused. Without --mcp, it also fails with exit code 1 when there's no kizen.json in the current directory.
block push and every appbuilder app command add the warning Claude files managed by appbuilder are out of date; run appbuilder setup-claude to their result's warnings when the plugin has any of the Claude skills and any managed file is missing or differs from the bundled one: either skill's files, or an existing kizenData.js. A plugin installed before a skill existed is therefore out of date until setup-claude adds it. A plugin with no Claude skill gets no warning. The warning never fails the command, and only headless runs show it.
appbuilder setup-codex
The same skills for Codex, which discovers repository skills in .agents/skills/<name>/SKILL.md: it installs or refreshes .agents/skills/kizen-custom-block/ (with design.md) and .agents/skills/kizen-business-apps/ in the plugin in the current directory. Both commands render the skills from the same templates; the Codex copies name setup-codex, .agents/skills and Codex's sandbox escalation where the Claude copies name setup-claude, .claude/skills and Claude Code's sandbox settings. appbuilder create writes both sets.
appbuilder setup-codex [--dry-run] [--include-lib]| Flag | Default | Purpose |
| --------------- | ------- | ------------------------------------------------------------ |
| --dry-run | off | Report what would change without writing files. |
| --include-lib | off | Also install the Kizen data helper library (kizenData.js). |
--include-lib, the per-file output, the summary (Codex files: …), the refusal to write through a symbolic link and the exit code 1 outside a plugin work as for setup-claude. The files are managed by the CLI, so a differing file is replaced without any flag. Inside Codex's sandbox .agents/ is read-only, so an agent running setup-codex there needs to escalate it.
block push and every appbuilder app command add Codex files managed by appbuilder are out of date; run appbuilder setup-codex under the same rule as the Claude warning, judged on the Codex skills; a plugin with both sets can get both warnings.
There's no --mcp: Codex doesn't take an MCP headers helper from its CLI (codex mcp add has no flag for it), so registering one means editing ~/.codex/config.toml by hand, and Codex gives the helper 10 seconds, which a first npx download of the pinned CLI can exceed.
appbuilder build
Reads the plugin in the current directory, validates it against the same rules enforced by the Kizen platform and Plugin Wizard, minifies sources, and writes .kizenapp/bundle.json. No flags.
If validation finds any errors (for example an api_name containing hyphens, which the platform rejects) the build fails and prints each issue grouped by file. Fix the reported issues and re-run.
Automation step configs are validated the same way — parameter data types, secrets that must be declared in the manifest's base_config.secrets, fields removed from the publish contract, and runtime — against the list of automation data types bundled in @kizenapps/packager. The Plugin Wizard applies the same rules at publish, against the live list read from the target environment, so a build that passes locally is expected to publish clean.
appbuilder dev
Starts the dev server and opens the viewer. Watches your plugin directory and rebuilds + hot-reloads the viewer on every change. Each rebuild runs the same validation as build.
| Flag | Default | Purpose |
| -------------------------- | ------- | ---------------------------------------------------------------- |
| -p, --port <port> | 3121 | Port the local dev server listens on |
| -c, --credentials <path> | — | Use a specific credentials JSON file instead of a stored profile |
| -d, --debug | off | Show a CDP event panel in the TUI |
| -v, --verbose | off | Log every CDP event and handled error (implies --debug) |
| --no-viewer | — | Don't auto-launch the viewer on startup (press v to launch it) |
| --no-cache | — | Disable the network proxy cache (always fetch upstream) |
The viewer and the proxy cache are both on by default; the two --no-* flags turn them off.
appbuilder encrypt
Encrypts a secret against a plugin's encryption keys and prints the envelope you paste into a kizen.json secret value:
{ "encrypted": true, "value": "<base64>" }The command talks to the Plugin Wizard host directly — appbuilder dev does not need to be running. It still uses API-key credentials (deprecated for block commands) until Plugin Wizard accepts sign-in sessions.
| Flag | Default | Purpose |
| -------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| -c, --credentials <path> | global credentials | Path to a credentials JSON file |
| -a, --api-name <name> | api_name from kizen.json in the cwd | Plugin the secret belongs to |
| -v, --value <value> | — | Plaintext secret. Prefer piping on stdin — a flag value is visible in ps. |
| -s, --stage <dev\|prod> | prod | Which encryption API to use. Defaulting is announced on stderr. |
| --remote | off (encrypt locally) | Have the wizard's /encrypt endpoint do the crypto with the keypair it holds, instead of fetching the public key and encrypting in-process. |
| -o, --out <path> | — | Also write the envelope to a file as pretty JSON |
Interactive by default. When either stdin or stdout is not a TTY (CI, a redirect, a pipe) it switches to a headless flow: everything must come from flags, the secret may be piped in, and the compact single-line envelope goes to stdout with all diagnostics on stderr.
printf %s "$SECRET" | appbuilder encrypt -a my_plugin -s prod > secret.jsonA failed --out write is a non-fatal warning — stdout already carries the envelope — but sets a non-zero exit code.
appbuilder report
Generates a self-contained, browsable report of the plugin in the current directory: the kizen.json config, a file tree, and every source file. Two files are written — an HTML report and a Markdown one (same path with a .md extension), the latter being useful as LLM context.
| Flag | Default | Purpose |
| --------------------- | --------------------------------------------- | ---------------- |
| -o, --output <path> | ~/.kizenappbuilder/examples/<api_name>.html | Output file path |
developer_business_id is stripped and every service's auth_credentials is redacted before rendering, so a report is safe to share.
appbuilder icons
Prints every valid icon name accepted by toolbar items, pages, and adornments, one per line. No flags — pipe it to a pager or grep it.
appbuilder icons | grep calendarappbuilder block export
Packages the plugin in the current directory (same validation and minification as build) and prints one block as pretty JSON — exactly what the Custom Block (AI Coded) dashlet's paste editor accepts. Nothing is written to .kizenapp.
appbuilder block export [api_name] [--copy]| Argument / flag | Default | Purpose |
| --------------- | ----------------------- | ---------------------------------------------------------------------- |
| [api_name] | the plugin's only block | Block to export. Required when the plugin has more than one block. |
| --copy | off | Also copy the JSON to the clipboard; a one-line status goes to stderr. |
Only the block JSON goes to stdout, so it can be piped or redirected (appbuilder block export > block.json). Validation warnings and errors go to stderr. The command fails with a non-zero exit code if validation fails, the block can't be found or is ambiguous, or the packaged block would be rejected by the paste editor (for example a block with no script.js, a non-positive-integer min_w, or a min_w greater than its max_w).
The JSON carries only the sizes the block's config.json writes: min_w, max_w, min_h, max_h, default_w and default_h. Sizes it leaves out are left out of the JSON too, rather than filled with packager defaults. default_w and default_h are the starting size, in grid columns and rows, of a dashlet created from the block. Each size that is set must be a positive integer, each min_* must be no greater than its max_*, and each default_* must fall within whichever of its min_* and max_* are set (for example min_w ≤ default_w ≤ max_w). Otherwise the export fails as invalid_block naming the field.
If the plugin has components in src/views/, the JSON also carries a views array: each packaged view's api_name, name, type, script/html/css (each left out when empty) and event_scripts (as a [{ name, script }] array), so the block can open one with this.showViewInModal('<api_name>'). Components in src/pages/ are not included. Without views, the output is unchanged.
appbuilder block push
Packages the plugin in the current directory (same validation as block export) and writes one block into a Custom Block (AI Coded) dashlet on a Kizen dashboard, homepage or chart group: it updates an existing dashlet or creates a new one. Card chrome (background, border, radius, shadow) comes from the dashboard's style settings, like native dashlets.
appbuilder block push [api_name] [flags]Interactive by default: pick the environment (only when you pass no --env and you're signed in to several, or the account you name is saved in several), the block, the surface (dashboard, homepage or chart group), the dashboard, then whether to update an existing custom code dashlet or create a new one, and confirm. Pushing to a production environment (go, fmo) asks you to type y. In a TTY, --yes skips the confirm prompt only when the headless write gate would apply, so go and fmo still ask for a typed y unless --allow-production is also passed; --dry-run shows the plan without writing. A dashlet edited in Kizen since the last push still prompts before overwriting unless --force is passed.
It runs headless when stdin or stdout is not a TTY, under a coding agent or CI, or with --json, --no-interactive or APPBUILDER_NO_INTERACTIVE (see Interactive or headless). --yes never selects the mode. Headless runs never prompt: anything that would be a question fails with needs_choice and lists the choices.
| Argument / flag | Default | Purpose |
| -------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| [api_name] | the plugin's only block | Block to push. Required when the plugin has more than one block. |
| -c, --credentials <path> | the sign-in session | API key (deprecated): credentials JSON file to use instead of the session |
| --profile <name> | the sign-in session | API key (deprecated): stored credential profile to use instead of the session |
| --dashboard <id> | the remembered target | Dashboard, homepage or chart group to push to |
| --dashlet <id> | matched by push history or name | Existing custom code dashlet to update. Needs --dashboard; can't be used with --create. |
| --create | off | Create a new dashlet instead of updating one |
| --dry-run | off | Resolve everything and print the request without writing |
| --yes | off | Write without asking. Headless runs without it are dry runs. |
| --allow-production | off | Allow headless writes to go / fmo |
| --force | off | Overwrite a dashlet that was edited in Kizen since the last push |
| --forget | off | Clear the remembered target for this block before resolving |
| --json | off | Print a JSON result to stdout (forces headless); the human summary goes to stderr |
| --no-interactive | off ($APPBUILDER_NO_INTERACTIVE) | Run headless even at a terminal |
| --env <env> | the only signed-in environment, or with --business the only one where it's saved | Environment of the session to use. Not with -c or --profile. |
| --business <id or name> | $APPBUILDER_BUSINESS, else the current account | Saved account to use, matched as for login --business. Not with -c or --profile. |
| --user <user id> | $APPBUILDER_USER | With --business, the user id of that saved account. Not with -c or --profile. |
Credentials. Without -c or --profile, the push uses your appbuilder login session for --env, or for the only environment you're signed in to. If you're signed in to several environments and pass no --env, the run is needs_choice with choice: "environment". It uses that environment's current account, unless --business (and --user), or APPBUILDER_BUSINESS (and APPBUILDER_USER), names another saved account; then the environment without --env is the only one where that name matches a saved account, whether or not any account there is current, and several such environments are needs_choice the same way (see Saved accounts). If you're signed out, it fails with not_signed_in, whose login.argv is the appbuilder login command to run; it never falls back to an API key. API keys (deprecated) are used only with -c <path> or --profile <name>, and never touch the session; --env, --business or --user with either is a usage_error, and APPBUILDER_BUSINESS and APPBUILDER_USER are ignored. The credentials file must name a valid environment (go, fmo, staging, integration, test1); a missing or invalid one is refused with credentials_invalid rather than defaulting to production. Empty apiKey, userId or businessId are refused the same way. A .kizenapp/config.json activeCredentialProfile no longer selects an API key: the push uses the session and prints a one-line notice to stderr, so --json output stays clean. credentialMode: "local" doesn't affect block commands.
Write gating (headless). --dry-run is always a dry run. Without --yes the run is a dry run too, and stderr says Dry run only: pass --yes to write. With --yes, go and fmo also need --allow-production (else production_requires_flag). A dry run's applyArgv is the command that writes what it showed: the same push with --yes, pinned to its dashboard and dashlet (--dashboard plus --dashlet or --create) and to the account the run signed in as, with --allow-production only on production, where requiresProductionConsent is true so an agent asks the user first. If the target dashlet was pushed from here before and its content has since been changed in Kizen, both dry runs and writes stop with drift_detected unless --force is passed.
Remembered target. A successful write records the target in .kizenapp/pushes.json (kept gitignored): one entry per environment, business, plugin and block. Pushing the block somewhere else replaces the entry. With no --dashboard, the next push goes to the remembered dashlet. If that dashboard or dashlet is gone, the push fails with remembered_target_missing and never silently creates a new one. When only the dashlet is gone the failure lists the dashboard's target choices, and --create adds a new block there; otherwise pass --dashboard <id> to choose another target, or --forget to clear it (honored in dry runs too; the result then carries "forgotten": true). A missing pushes.json just means nothing is remembered yet; one that can't be read, isn't valid JSON, or isn't a JSON array fails the push with local_error before anything is written, so fix or delete it.
Size. A dashlet created by push starts default_w columns wide, or min_w when there's no default_w, or 6 when neither is set. Its height works the same way with default_h, then min_h, then 3 rows. That size is clamped into whichever of min_*/max_* the block's config.json sets, then into the grid (2 to 12 columns, 1 to 99 rows). The pushed content carries exactly the sizes config.json writes, the same as block export. Updating an existing dashlet never changes its layout or size.
The pushed content includes the plugin's src/views/ components as views, in the same shape as block export. They count toward drift detection: a view changed in Kizen since the last push stops the push with drift_detected. Blocks in plugins without views push exactly as before, and existing pushes.json entries stay valid.
block export and block push fail with invalid_block if a view's api_name, name, script, html, css or event script contains a NUL character or {__ref:. The field is reported as views.<api_name>.<field>. A warning appbuilder/views-unmatched appears if some src/views/ components couldn't be matched to packaged views.
Matching. With --dashboard and neither --dashlet nor --create, the push updates the dashlet recorded in .kizenapp/pushes.json for that dashboard, else the single custom code dashlet named appbuilder:<plugin_api_name>/<block_api_name> (the name given to every dashlet push creates). No match, or several, is needs_choice.
JSON output
With --json, every result is one pretty-printed JSON object on stdout, and the human summary goes to stderr. Headless without --json prints human text instead: success and dry-run output on stdout, errors on stderr. A dry run:
{
"ok": true,
"applied": false,
"dryRun": true,
"reason": "no_yes",
"action": "update",
"environment": "staging",
"businessId": "b0c6…",
"dashboard": { "id": "5f1e…", "name": "Sales overview", "type": "dashboard" },
"dashletId": "9a2d…",
"url": "https://v2.staging.kizen.com/dashboard/5f1e…",
"resolvedBy": "remembered",
"block": {
"pluginApiName": "my_plugin",
"apiName": "pipeline_summary",
"name": "Pipeline summary"
},
"method": "PATCH",
"path": "/dashboards/5f1e…/dashlet/9a2d…",
"body": { "config": { "…": "…" } },
"warnings": [],
"applyArgv": [
"appbuilder",
"block",
"push",
"pipeline_summary",
"--env",
"staging",
"--business",
"b0c6…",
"--user",
"7d1f…",
"--dashboard",
"5f1e…",
"--dashlet",
"9a2d…",
"--yes",
"--json"
],
"requiresProductionConsent": false
}reason is flag (--dry-run) or no_yes (headless without --yes). action is create or update, method is POST or PATCH, and dashletId is null for a create. resolvedBy is flag, remembered, push_map or name. dashboard.type is dashboard, homepage or chart_group.
An applied write:
{
"ok": true,
"applied": true,
"dryRun": false,
"action": "created",
"environment": "staging",
"businessId": "b0c6…",
"dashboard": { "id": "5f1e…", "name": "Sales overview", "type": "dashboard" },
"dashletId": "c41b…",
"url": "https://v2.staging.kizen.com/dashboard/5f1e…",
"refresh": {
"dashboardId": "5f1e…",
"script": "await window.__kizenCustomBlocks?.refresh('5f1e…')"
},
"resolvedBy": "flag",
"block": {
"pluginApiName": "my_plugin",
"apiName": "pipeline_summary",
"name": "Pipeline summary"
},
"remembered": true,
"warnings": []
}action is created or updated. remembered is false (with a warning) when .kizenapp/pushes.json couldn't be written; the push itself still succeeded.
refresh is only on applied writes, and only when the dashboard id is a uuid; otherwise it's left out. script is exactly await window.__kizenCustomBlocks?.refresh('<dashboardId>'). Run it in a Kizen tab that already has url open to refetch that dashboard in place instead of reloading the page. The hook is registered on dashboards, homepages and chart groups. When it's available on the page, the script resolves to { refreshed: true, dashboardId }. Otherwise it resolves to undefined: the page is an older Kizen build without the hook. refreshed: true means the dashboard query was invalidated, not that the target was on screen, which is why the agent matches the tab by url and screenshots afterwards. Human output prints the same script on a Refresh an open tab: line after the URL.
A failure exits 2 when it stops for a decision (needs_choice) and 1 otherwise:
{
"ok": false,
"code": "needs_choice",
"message": "Choose a block to update on \"Sales overview\", or create a new one; pass --dashlet <id> or --create.",
"choice": "target",
"choices": [
{
"value": "9a2d…",
"label": "Pipeline summary",
"args": ["--dashboard", "5f1e…", "--dashlet", "9a2d…"],
"pushKey": "appbuilder:my_plugin/pipeline_summary",
"isFromThisPlugin": true,
"argv": [
"appbuilder",
"block",
"push",
"pipeline_summary",
"--env",
"staging",
"--business",
"b0c6…",
"--user",
"7d1f…",
"--dashboard",
"5f1e…",
"--dashlet",
"9a2d…",
"--json"
]
},
{
"value": "new",
"label": "Create a new block",
"args": ["--dashboard", "5f1e…", "--create"],
"argv": [
"appbuilder",
"block",
"push",
"pipeline_summary",
"--env",
"staging",
"--business",
"b0c6…",
"--user",
"7d1f…",
"--dashboard",
"5f1e…",
"--create",
"--json"
]
}
]
}Optional failure fields: choice (environment, block, dashboard or target) and choices with needs_choice, remembered_target_missing and a --dashlet that is not_found; issues (the packager's validation issues) with validation_failed; field with invalid_block; hint (what to do next) with not_signed_in, auth_failed, forbidden, session_store_unwritable and others; login ({ "argv": [...] }, the appbuilder login command for the user to run) with not_signed_in; and sandbox ("codex_network_disabled") on a network_error when Codex's sandbox has the network turned off. Each choice's argv is the whole command that selects it: the push re-run with that choice and the account flags (--json too when the run had it), without --yes, so it's a dry run whose applyArgv then writes. Run it as given. args are only the flags that select it (dashboard choices also carry surface and canEdit).
| Code | Meaning |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| needs_choice | An environment, block, dashboard or target must be chosen; see choice and choices |
| validation_failed | The plugin failed validation |
| invalid_block | The plugin has no blocks, or the block would be rejected (field names the problem) |
| not_signed_in | No usable sign-in session (missing, expired, rejected or unreadable); login.argv is the appbuilder login command for the user to run |
| credentials_invalid | The -c/--profile file can't be read, its environment is missing or invalid, or it lacks apiKey, userId or businessId |
| auth_failed | Kizen rejected the credentials |
| forbidden | The credentials can't access the dashboard |
| not_found | The dashboard, or the --dashlet on it, doesn't exist |
| remembered_target_missing | The remembered dashboard or dashlet is gone; nothing is created without --create |
| drift_detected | The dashlet was edited in Kizen since the last push; --force overwrites |
| production_requires_flag | --yes against go / fmo without --allow-production |
| usage_error | Conflicting flags (--dashlet with --create, --dashlet without --dashboard), a non-custom-code --dashlet, or a --business (or APPBUILDER_BUSINESS) that matches no saved account, or several |
| network_error | Kizen couldn't be reached; sandbox: "codex_network_disabled" when Codex's sandbox has the network off |
| session_store_unwritable | The session couldn't be saved to ~/.kizenappbuilder/oauth/ (a sandbox, or a full disk); hint says how to allow it |
| api_error | Any other Kizen API failure |
| local_error | A local failure that isn't a validation error: no kizen.json, packaging, reading .kizenapp/pushes.json, or writing it for --forget |
Sign-in sessions add these cases: not being signed in, or a session that needs a new appbuilder login (expired, rejected or unreadable), is not_signed_in with login.argv (appbuilder login, plus --env when the environment is known and --business/--user when the run named an account); several signed-in environments and no --env is needs_choice (choice: "environment", each choice { value, label, args: ["--env", <env>], argv }), and so is a --business (or APPBUILDER_BUSINESS) that matches saved accounts in several environments, whose retry commands keep --business and --user; --env, --business or --user with -c or --profile, or an unknown --env, is usage_error; a --business (or APPBUILDER_BUSINESS) that matches no saved account, or several, is usage_error with a message that lists them, and so is --user without --business (or APPBUILDER_USER without APPBUILDER_BUSINESS); an unreachable token endpoint is network_error; a refreshed session that can't be saved is session_store_unwritable; and any other failure to refresh the session is api_error.
appbuilder block targets
Lists where a block can be pushed: the business's dashboards and homepages plus its custom objects, or with --object one custom object's chart groups. Always headless.
The CLI honors HTTP_PROXY/HTTPS_PROXY and NO_PROXY on Node versions that support it (24.14+, or with NODE_USE_ENV_PROXY=1 set).
customObjects is sorted by name and ends with Contacts ("fetchUrl": "client") when the CLI can look up the business's contacts object; Kizen doesn't list Contacts with the other custom objects, so if that lookup fails, Contacts isn't included. --object <contacts id> lists Contacts' chart groups like any other object's.
| Flag | Default | Purpose |
| -------------------------- | ------------------- | ------------------------------------------------------------- |
| -c, --credentials <path> | the sign-in session | API key (deprecated): credentials JSON file to use |
| --profile <name> | the sign-in session | API key (deprecated): stored credential profile to use |
| --object <id> | — | List the chart groups of this custom object (or Contacts) |
| --json | off | Print JSON instead of tables |
| --env <env> | as for block push | Environment of the session to use. Not with -c/--profile. |
| --business <id or name> | as for block push | Saved account to use. Not with -c/--profile. |
| --user <user id> | as for block push | With --business, the user id of that saved account. |
Credentials, --env, --business and --user work as for block push (see appbuilder login).
{
"ok": true,
"environment": "staging",
"businessId": "b0c6…",
"surfaces": {
"dashboard": [
{
"id": "5f1e…",
"name": "Sales overview",
"type": "dashboard",
"dashletsCount": 4,
"employeeAccess": "Owner",
"hidden": false,
"canEdit": true
}
],
"homepage": []
},
"customObjects": [
{ "id": "71d0…", "objectName": "Deals", "fetchUrl": "pipeline" },
{ "id": "c3a9…", "objectName": "Contacts", "fetchUrl": "client" }
]
}With --object <id> the shape is { ok, environment, businessId, object: { id, objectName, fetchUrl } | null, surfaces: { chart_group: [...] } }. canEdit is true for Owner, Admin or Edit access and null when Kizen reports no access level. Failures use the same shape and codes as block push.
Agent loop
The sequence a coding agent follows to build a block and put it on a dashboard. After upgrading the CLI, run appbuilder setup-claude (or appbuilder setup-codex) so the agent's skill matches the new commands.
appbuilder block export <api_name>
appbuilder block targets --json
appbuilder block targets --object <object_id> --json # chart groups only
appbuilder block push <api_name> --dashboard <id> --json # needs_choice → choices (existing blocks + new)
appbuilder block push <api_name> --dashboard <id> [--dashlet <id> | --create] --dry-run --json
appbuilder block push <api_name> --dashboard <id> [--dashlet <id> | --create] --yes --json
# change requests: edit, re-validate, then
appbuilder block push <api_name> --yes --json # remembered target, zero questionsAfter each applied push, an agent that can drive the user's browser refreshes the open tab instead of reloading it: find the tab whose origin and path match url, run refresh.script there, and check that the block rendered. If the script returns undefined, the hook isn't available on that page, so navigate the tab to url instead. If no tab has url open, open it in a new one. refresh is absent when the dashboard id isn't a uuid; navigate to url in that case.
Guardrails:
- Never pass
--allow-productionunless the user named production. - Stop on
drift_detectedand ask the user before retrying with--force. - On
needs_choice(exit 2), ask the user usingchoices(labels to show), then run the chosenargvexactly as given. - Run a dry run's
applyArgvas given, and only after the user agreed; whenrequiresProductionConsentistrue, only after they agreed to write to production. - On
not_signed_in, ask the user to runlogin.argv(it opens a browser). - On
remembered_target_missing, ask whether to--createa new block rather than creating one.
appbuilder app
Publishes the plugin in the current folder as a Kizen Business App on the business you're signed in to. Each developer pushes their own preview of the app; publishing a preview makes it the next Live version, which every user of the business gets. The server numbers the releases (kizen.json version isn't sent).
appbuilder app versions # Live, released and preview versions, and this folder's base
appbuilder app push [--force] [--base-from-preview <id>] # save this folder as your preview
appbuilder app publish [--force] [--expect-preview <id>] # make your preview the next Live version
appbuilder app merge [--release <n>] [--base <n> | --accept-live-base] # merge Live (or release n) into this folder
appbuilder app rollback <n> # restore released version n as your preview
appbuilder app pull <n> [--out <dir>] # download release n's source into a new folder
appbuilder app pull --preview [--out <dir>] # download your preview's source into a new folder
appbuilder app actions list # the app's JS actions and the objects they're linked to
appbuilder app actions link <action> <object> [--menu | --no-menu] # link a JS action to an object
appbuilder app actions unlink <action> <object> # remove that link
appbuilder app preview delete [--owner <userId>] [--expect-updated <ts>] [--delete-app] # delete your preview (or another developer's)At a terminal each command is interactive: it shows the plan and asks before writing, and production (go, fmo) needs a typed y. It runs headless when stdin or stdout is not a TTY, under a coding agent or CI, or with --json, --no-interactive or APPBUILDER_NO_INTERACTIVE (see Interactive or headless).
| Flag | Commands | Purpose |
| ------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| -c, --credentials <path> | all | API key (deprecated): credentials JSON file to use instead of the session |
| --profile <name> | all | API key (deprecated): stored credential profile to use instead of the session |
| --dry-run | push, publish, merge, rollback, actions link/unlink, preview delete | Show what would change without writing |
| --yes | push, publish, merge, rollback, actions link/unlink, preview delete | Write without asking. Headless runs without it are dry runs. |
| --allow-production | push, publish, rollback, actions link/unlink, preview delete | Allow headless writes to go / fmo (merge writes only local files, so it has none) |
| --menu, --no-menu | actions link | Show the action in the object's Perform Action menus, or not. A new link isn't shown unless --menu; an existing link keeps its setting unless one is passed. |
| --force | push | Push even though files still have merge conflict markers |
| --base-from-preview <id> | push | Take this folder's base from your preview, while your preview is still source package <id>. Only a base_required choice prints it (see base_required) |
| --force | publish | Publish even though Live changed since your preview was based on it, or its base is unknown. Never publishes a preview pushed again since it was reviewed (preview_replaced) |
| --expect-preview <id> | publish | Publish only if your preview's source package is still <id> (a dry run's applyArgv carries it) |
| --release <n> | merge | Merge release <n> instead of Live |
| --base <n> | merge | The release this folder was copied from, when no base is recorded. A wrong one drops the changes made since then. |
| --accept-live-base | merge | Record Live (or --release) as this folder's base without merging, after merging by hand |
| --preview | pull | Download your preview instead of a released version. Pass <n> or --preview, not both |
| --out <dir> | pull | Folder to create (default ../<api_name>-v<n>, or ../<api_name>-preview with --preview, next to the plugin folder) |
| --owner <userId> | preview delete | Delete this user's preview instead of yours (a user id, as versions lists in previews[].previewOwner.id) |
| --expect-updated <ts> | preview delete | Delete only if the preview's last update is still <ts> (a dry run's applyArgv carries it) |
| --delete-app | preview delete | Allow deleting the whole app when the preview is its only version (a dry run's applyArgv carries it only when it warned about that) |
| --json | all | Print one JSON result to stdout (forces headless); the human text goes to stderr |
| --no-interactive | all | Run headless even at a terminal (also $APPBUILDER_NO_INTERACTIVE) |
| --env, --business, --user | all | The session and saved account to use, as for block push |
Credentials work as for block push: the appbuilder login session by default, an API key only with -c or --profile. Business Apps need the Marketplace › Business App Developer permission at the Delete/All level; without it the commands fail with forbidden and say so in hint.
Workflow
appbuilder app versionsshows where things stand. Itsnextismergewhen Live moved past this folder's base, elsepushthenpublish.appbuilder app pushsaves this folder as your preview. Other developers' previews and Live don't change.appbuilder app publishmakes your preview the next Live version (nextReleaseAtLeastin the dry run). Before promoting it, it checks that Live hasn't moved since your preview's base, both in the dry run and again just before the write. Your preview's base is read from your preview on the server (see Moving to another machine), so it doesn't matter which folder or machine pushed it.- When someone else published in between, publish fails with
publish_conflict: runappbuilder app merge, resolve any conflict markers, then push and publish again.
The first push of an app that isn't on the business yet creates it (published: false in versions until then). A release needs a thumbnail; publishing a preview without one fails with thumbnail_required.
Headless write gate and JSON results
push, publish, merge, rollback and preview delete are dry runs headless unless --yes is passed, and --yes against go or fmo also needs --allow-production (else production_requires_flag). merge only writes local files, so it needs --yes but never --allow-production. pull writes only into a new folder and needs neither. versions is read-only.
A dry run:
{
"ok": true,
"applied": false,
"dryRun": true,
"reason": "no_yes",
"action": "push",
"environment": "staging",
"businessId": "b0c6…",
"userId": "7d1f…",
"app": { "baseApiName": "acme", "apiName": "acme_abc123", "name": "Acme", "liveVersion": "5" },
"source": { "files": 12, "mode": "git", "bytes": 18342, "hash": "dfc2…" },
"thumbnail": true,
"preview": { "exists": true, "updated": "2026-10-20T00:00:00.000Z" },
"base": { "version": "5", "source": "recorded", "liveMoved": false },
"conflictMarkers": [],
"notices": [],
"warnings": [],
"applyArgv": [
"appbuilder",
"app",
"push",
"--yes",
"--env",
"staging",
"--business",
"b0c6…",
"--user",
"7d1f…",
"--json"
],
"requiresProductionConsent": false,
"next": []
}applyArgv is the command that applies what the dry run showed: the same command with --yes, plus --allow-production on go or fmo, pinned to the account the run signed in as (--env, --business, --user, or the -c/--profile it was given) so that a switch to another account meanwhile can't redirect it. A publish dry run pins the preview it reviewed (--expect-preview <id>), a merge dry run the version it planned (--release <n>, plus --base <n> when the base came from the flag or was detected), rollback its version, and preview delete the preview's owner (--owner <userId>, even for your own), the preview it reviewed (--expect-updated <ts>) and, only when it warned that the app goes too, --delete-app. A push apply uploads the folder as it is when it runs. requiresProductionConsent is true exactly when applyArgv carries --allow-production: an agent asks the user before running it. A dry run's next is empty, so the only way on is applyArgv. A push's base.source says where its base came from: recorded (this folder's state), never_published (no Live version, so none is needed), detected (the folder matches a release exactly), preview (it matches your preview exactly, and the base is your preview's, read from the server) or base_from_preview (--base-from-preview). A publish's conflictCheck compares your preview's base, read from its lineage on the server, with Live: baseLiveVersion and baseSourcePackageId are the lineage's, and reason says why it's a conflict.
An applied write has applied: true, the command's results (previewVersion, sourcePackageId and thumbnailId for a push; liveVersion and previousLiveVersion for a publish; status and the file lists for a merge) and next, the commands that follow, each { "step", "argv" }: a push is followed by a publish dry run (after the JS action link steps, when any action may need linking; see app actions), a rollback by a publish dry run, a merge by push and publish dry runs. A merge that left conflict markers starts next with a step that has no command:
{ "step": "resolve", "files": ["src/a.js"], "markers": { "local": "HEAD", "incoming": "live" } }remembered is false (with a warning) when .kizenapp/apps.json couldn't be written; the write itself still succeeded.
A push adds the CLI's entries to .gitignore when they're missing and then zips the folder again, so the applied result's source describes the zip that was uploaded. If any file other than .gitignore changed since the plan (an edit made while the confirm was open, say), the push stops with local_error and uploads nothing; run it again to review the change.
A publish that can't confirm that the release is the preview it reviewed (the preview was pushed again from elsewhere, or the check failed) still publishes, but says in warnings that this folder's base wasn't advanced. The next publish from this folder then stops with publish_conflict; merge, push and publish again.
Failures and exit codes
With --json a failure is { "ok": false, "code", "message", "hint"?, … } on stdout. Exit 0 is success, dry runs included; exit 2 means the command stopped for a decision (needs_choice or base_required) and its choices say how to go on; every other failure exits 1. Each choice carries argv, the whole command to run as given: --json and the run's account flags are already in it.
{
"ok": false,
"code": "base_required",
"message": "No base version is recorded for this folder, and Live is v3. Merge Live into this folder, or record that it already contains Live, then push.",
"choice": "base",
"choices": [
{
"value": "merge",
"label": "Preview merging Live v3 into this folder",
"args": [],
"argv": [
"appbuilder",
"app",
"merge",
"--release",
"3",
"--env",
"staging",
"--business",
"b0c6…",
"--user",
"7d1f…",
"--json"
]
},
{
"value": "accept",
"label": "This folder already contains Live v3",
"args": [],
"argv": [
