Skip to content

Board configuration

Kangentic stores your board layout in a kangentic.json file at the root of each project. This file is designed to be committed to git so your entire team shares the same columns and the automations on them.

FilePurposeGit tracked?
kangentic.jsonTeam-shared board layoutYes
kangentic.local.jsonPersonal overridesNo (auto-added to .gitignore)

Effective config = deep-merge(kangentic.json, kangentic.local.json). Local values win when both files define the same column. A column’s automations merge as a whole list, not row by row: a local column that declares automations replaces the team file’s list for that column outright, because an ordered list has no stable key to merge a row against once names can change.

Beyond columns, the team file can carry shortcuts (see Command shortcuts), defaultBaseBranch (the team-shared branch tasks start from - the ref a worktree is cut from, and the one a non-worktree task’s branch checkout is fast-forwarded from; see Base branch), and profiles - named Board profiles. Profiles are team-only by design: a task’s saved profile choice must resolve on every teammate’s machine, so they never go in kangentic.local.json.

{
"version": 1,
"columns": [
{
"id": "e6350e8a-...",
"name": "To Do",
"role": "todo",
"icon": "layers",
"color": "#6b7280"
},
{
"name": "Planning",
"icon": "map",
"color": "#8b5cf6",
"autoSpawn": true,
"permissionMode": "plan",
"planExitTarget": "Executing"
},
{
"name": "Executing",
"icon": "square-terminal",
"color": "#3b82f6",
"autoSpawn": true
},
{
"name": "Code Review",
"icon": "code",
"color": "#f59e0b",
"sessionTarget": "isolated",
"sessionSpawnStrategy": "always_spawn_new",
"automations": {
"onEnter": [
{ "name": "Review", "type": "send_message", "message": "/code-review {{baseBranch}}" },
{ "name": "Ping the channel", "type": "webhook", "url": "https://hooks.example.com/abc" }
],
"onTurnEnd": [
{
"name": "Back for revisions",
"type": "move_to_column",
"finalLine": "Verdict: Blocked",
"maxRuns": 3,
"targetColumn": "Executing",
"message": "Code Review sent this back. Fix what it found.\n\n{{agentReply}}"
},
{ "name": "Finish", "type": "move_to_column", "condition": "The review left nothing to fix" }
],
"onExit": [
{ "name": "Archive the log", "type": "run_script", "enabled": false, "script": "scripts/archive.sh" }
]
}
},
{
"name": "Done",
"role": "done",
"icon": "circle-check-big",
"color": "#10b981"
}
]
}
FieldTypeDescription
idstringAuto-generated UUID. Enables precise reconciliation - leave blank when hand-writing.
namestringDisplay name (must be unique)
descriptionstringFree-form note on the column’s purpose. Shown as a header tooltip and shared with your team via kangentic.json. Omit when unset
rolestring"todo" or "done" - assigns special behavior. Omit it for a custom column. Any other value is treated as a custom column and, when the file is applied to an open project, raises a warning on the board naming the column
iconstringLucide icon name (e.g., "layers", "square-terminal")
colorstringHex color for column header
autoSpawnbooleanSpawn an agent when a task arrives
permissionModestringPermission mode override ("default", "plan", "acceptEdits", "dontAsk", "auto", "bypassPermissions")
planExitTargetstringTarget column name when a plan-mode agent exits
automationsobjectWhat the column does when a task enters, when its agent ends a turn, and when the task leaves: three named arrays, onEnter, onTurnEnd, and onExit. See Automations below
agentOverridestringUse a specific agent CLI for sessions in this column: "claude", "codex", "gemini", "cursor", "copilot", "grok", "antigravity", "goose", "opencode", "aider", "qwen", "kimi", "droid", "warp", or "ollama". Omit to inherit the project’s default agent
modelOverridestringAdapter-specific model identifier passed at spawn (e.g. opus for Claude, gpt-5-codex for Codex), stored exactly as written. A floating alias such as opus runs the latest release of that model, so a committed column moves to each new version with no edit, while a full id stays pinned. When a running session moves into a column with a different modelOverride, Kangentic suspends it and resumes it on the new model with its conversation intact. Hidden in the Column Manager when the active CLI’s capability probe doesn’t expose a --model flag
effortOverridestringAdapter-specific effort/reasoning level (e.g. low, medium, high, xhigh, max for Claude). When a running session moves into a column with a different effortOverride, Kangentic suspends it and resumes it with the new --effort, as it does for a model. Hidden when the adapter’s capability probe doesn’t expose effort levels
handoffContextbooleanPoint the new agent at the prior agent’s native session history file on column entry - enables cross-agent context handoff. Any agent can receive one; whether context carries depends on the outgoing agent. Claude, Codex, Gemini, Grok, Antigravity, OpenCode, Aider, Qwen Code, Kimi Code, and Factory Droid leave a readable history, while Cursor, Copilot, Goose, Warp, and Ollama leave nothing Kangentic can hand on
sessionTargetstringWhich session a task runs on in this column: "main" (default - the task’s shared main conversation) or "isolated" (this column’s own separate session, keyed by the column - it does not inherit the main conversation, which suits work like a code review). Only written to the file when set to "isolated"
sessionSpawnStrategystringWhat the column does with that session on entry: "create_or_resume" (default - resume an existing session, else spawn) or "always_spawn_new" (a fresh, independent pass each entry). The Column Manager and the MCP column tools no longer offer this field directly; they set it for you when sessionTarget changes (isolated goes to "always_spawn_new", main goes back to "create_or_resume"). A hand edit does not: the file is applied key by key, so an isolated column you write by hand needs both keys, or it resumes one long session instead of running a fresh pass per entry. The one reason to write this by hand is the other combination, "isolated" with "create_or_resume": a persistent side conversation for the column that resumes across entries. Only written to the file when it differs from the default

Each column owns one ordered list of automations, split into three groups. In the file they nest under the column as automations.onEnter (On task enter), automations.onTurnEnd (On agent turn end), and automations.onExit (On task exit). Array order is the run order within its group; an empty group is an absent key, and a column with no automations has no automations key at all.

Each row carries name, type, an optional enabled, its guards, and the type’s own fields flat on the row, not under a nested object. enabled is omitted when true, so a switched-off row is the only one that carries it. A name is required and must be unique within its column, ignoring case; that is enforced by the database, not only the editor, so a hand-edited file cannot break it.

TypeFields
send_messagemessage (the text, with template variables), mode ("immediate", the default, or "deferred"; no effect in onTurnEnd, where the turn is already over)
run_scriptscript (the body, with template variables), timeoutMinutes (1 to 120, default 10; set here only, the Column Manager does not offer it)
webhookurl, method ("POST", the default, "GET", or "PUT"), body (empty for the default JSON payload), headers (an object of header names to values)
notifytitle (default {{title}}), body (default {{toColumn}})
move_to_columntargetColumn (a column’s name; absent or empty is Next column), message (optional, for the agent in that column, with template variables). In onTurnEnd only

Any row can also carry the two guards from the Condition and frequency cards:

KeyTypeMeaning
conditionstringAn Agent evaluation: plain words the Automation agent judges before the row runs
finalLinestringA Text match: a pattern checked against the last line of the agent’s final message, * as a wildcard. In onTurnEnd only
maxRunsnumberFrequency: the most times the row runs on one task, a whole number of at least 1. The count starts over when the task leaves Done

A row holds one kind of Condition, so a written row carries condition or finalLine, never both. The Configuration reference lists the same keys beside the column’s other settings.

Every text field takes the template variables. The array a row sits in is the only thing that decides when it runs; a hand-written "on" key is dropped on read, like any other key the type does not declare. To run the same automation on enter and on exit, write it in both arrays.

Malformed rows never fail the file. A row with an empty name, an unknown type, or a retired type is skipped with a warning, and a second row with a name already taken in that column is renamed with a suffix and warned about, so one bad line cannot take the board down on project open. The guards are read the same forgiving way, each with a warning:

  • A move_to_column row under onEnter or onExit is skipped.
  • A finalLine under onEnter or onExit is ignored, and the row runs without it.
  • A row with both condition and finalLine keeps the finalLine.
  • A finalLine with no letter, number, or * is kept, but no line can match it, so the row never runs.
  • A condition or finalLine that is not text, or a maxRuns that is not a number, is read as absent, so the row runs without that guard.
  • A maxRuns that is not a whole number of at least 1 is rounded down, and one below 1 means no limit.

Two older spellings are accepted on read and never written back: send_command as an alias for send_message (with command as an alias for message), and the legacy spawn_agent type, which survives on boards that already had one carrying a custom prompt but cannot be created. The retired action types (kill_session, create_worktree, cleanup_worktree) are warned about and skipped. Since v0.44.0 an automations value that is not an object, or a group (onEnter, onTurnEnd, or onExit) that is not a list (a single row written without brackets, say), leaves that column’s current automations untouched instead of reading as empty and deleting them. On project open these warnings appear in the board’s warning banner; an apply while the app is running shows each as a toast.

Since v0.46.0, Kangentic keeps what it does not understand when it writes the file back: a row of a type it does not know, a key it does not know on a row it does, and a group it has no name for. It runs none of them, but a teammate on an older version no longer strips a newer version’s automations from the next commit. A version before v0.46.0 has no such rule and does not know onTurnEnd, so it drops turn-end rows the next time it exports. Get the team onto v0.46.0 before committing them.

Kangentic automatically exports your board to kangentic.json whenever you make changes in the UI - adding columns, editing automations, reordering, etc. You don’t need to manually maintain this file.

The sync is bidirectional, and the file wins: on every project open, Kangentic first applies the file’s state to the database - unprompted, before exporting - so hand-editing kangentic.json is a genuine way to change the board, not just a record of it. The confirmation banner covers only an external edit that lands while the project is already open.

The export uses atomic writes (temp file + rename) so the file is never left in a half-written state.

Since v0.44.0 a kangentic.json that cannot be read, such as one holding git merge conflict markers, is never overwritten. The board loads from the local database, a banner says the file needs fixing, and nothing writes to the file until it is fixed: the export on open and after a board edit are skipped, and a profile, shortcut, or base branch save is refused with an error rather than reported as saved. Board edits you make meanwhile stay in the database, and the fixed file applies over them once it changes on disk. An empty file reads as missing and is re-created, and a leading UTF-8 byte order mark from a Windows editor is ignored. An unreadable kangentic.local.json is reported the same way, and the team file applies without it.

  1. Open your project in Kangentic and configure your board
  2. Kangentic auto-exports kangentic.json to the project root
  3. Commit kangentic.json to git and push

When a teammate pushes an updated kangentic.json:

  1. Pull the changes - Kangentic’s file watcher detects the update automatically
  2. A reconciliation banner appears asking to apply the new config
  3. Click Apply to sync the changes into your board

Kangentic watches the kangentic.json file for changes in real time. Any external modification - a git pull, a hand edit, or another tool - triggers the reconciliation prompt, including a commit you made elsewhere and pulled back onto this machine. Kangentic’s own UI exports write back silently, so they never prompt. Your board stays in sync with your team without any manual import/export steps.

By default, an incoming kangentic.json change waits behind the reconciliation banner so you can review it before it touches your board. To skip the banner and apply detected changes immediately, turn on Auto-apply board config changes under Settings > Board (config key skipBoardConfigConfirm, default off, global-only).

This suits single-author projects and high-trust teams that always want the latest layout. Personal overrides in kangentic.local.json never prompt regardless of this setting - they reload silently.

If you want to customize a column color or add a personal column without affecting the team file:

  1. Make changes in the UI
  2. The team-shared properties go to kangentic.json
  3. Create kangentic.local.json for overrides you want to keep private

kangentic.local.json is automatically added to .gitignore.

Hand-written configs (no id fields) are treated as additive - Kangentic creates missing columns but never deletes existing ones. This is safe for bootstrapping a new team.

Auto-exported configs (with id fields) are treated as canonical - columns in the database that don’t appear in the config are removed or ghosted. This enables full two-way sync.

When a teammate removes a column from kangentic.json but you still have tasks in that column:

  • The column becomes a ghost: hidden from the board but preserved in the database
  • Your tasks are safe; move them to another column
  • Once empty, a ghost column is cleaned up on the next reconciliation

Note the asymmetry with in-app removal: the Column Manager and the MCP delete tool refuse to remove a non-empty column outright, while a file edit softly ghosts it instead. A file edit also does not prune Board profile references to the removed column; the in-app and MCP paths do.

Every board must have a To Do column (role: "todo") and a Done column (role: "done"). If your config omits them, Kangentic creates defaults. To Do is always first; Done is always last.

A project with no kangentic.json at all does not start with just those two: Kangentic seeds the full seven-column default board (To Do, Planning, Executing, Code Review, Testing, Merge, Done). A team file replaces that seed with whatever columns it declares.

Minimal kangentic.json to bootstrap a new project with a plan-then-execute workflow (an example layout, deliberately smaller than the seeded default):

{
"version": 1,
"columns": [
{ "name": "To Do", "role": "todo" },
{ "name": "Planning", "permissionMode": "plan", "planExitTarget": "Executing", "autoSpawn": true },
{ "name": "Executing", "autoSpawn": true },
{
"name": "Code Review",
"automations": {
"onEnter": [
{ "name": "Review", "type": "send_message", "message": "Review the diff on this branch and fix what you would change." }
]
}
},
{ "name": "Done", "role": "done" }
]
}

Drop this file in a repo root, open the project in Kangentic, and the board is ready.

Kangentic is free and open source. A star helps other people find it.

Star on GitHub