Skip to main content

Project file

A kapi project is a portable kapi.yaml recipe at a project's root that captures a content workflow: which files count as content, the voice profile, terms and coordinates that govern them, source and target languages, tool pipelines (flows), plugin requirements, and processing defaults. It carries no credentials or state, so it is always safe to commit. For when to use a project versus an ad-hoc run, see Projects.

Discovery

Like git, kapi walks up the directory tree from where you run it and uses the nearest kapi.yaml recipe, running in project mode with that project's languages, defaults, and flows. A directory holds at most one kapi.yaml, so discovery is unambiguous. An explicit -p <path> overrides discovery, and KAPI_NO_PROJECT=1 disables it so the CLI runs ad-hoc.

Format

version: v1
id: prj_k3m7xq2vw9ta5r8ybn4dcp
name: My App

collections:
- path: "src/locales/en/*.json"
target: "src/locales/{lang}" # mirror each matched file under the per-language dir

preset: nextjs
plugins:
okapi: "^1.47.0"

flows:
translate:
steps:
- tool: translate
config:
provider: anthropic
model: <provider-model-id>

translate-and-qa:
steps:
- tool: translate
config:
provider: anthropic
- tool: qa

pseudo:
steps:
- tool: pseudo-translate
config:
expansionPercent: 30

defaults:
source_language: en
target_languages:
- fr
- de
- ja
concurrency: 4
parallel_blocks: 3
encoding: utf-8
exclude:
- "**/*.generated.json"
merge:
conflict_policy: translator-wins
memory:
fuzzy_threshold: 75
segmentation:
source: true
terms_source: .kapi/terms.json
memory_source: .kapi/memory/memory.json

Fields

Required

FieldTypeDescription
versionstringSchema version, must be "v1"
namestringProject display name

Optional

FieldTypeDescription
idstringThe project's stable identity (see Project identity)
collectionsCollection[]The content this project governs (see Collections)
profilesmapGovernance bound per product, keyed by profile name (see Profiles and channels)
presetstringA framework preset name (see kapi init --list-presets) (e.g., nextjs, react-intl) applied to formats
pluginsmapPlugin requirements keyed by name (e.g., okapi: "^1.47.0")
requiresmapPlugin name → semver constraint that gates loading; the recipe refuses to load without a registered plugin
flowsmapNamed flow definitions (see Flow Steps)
defaultsDefaultsProcessing defaults (see Defaults; source/target languages live here)
ship_gateGateA single catch-all ship gate (see Ship gates)
ship_gatesShipGateRule[]Scoped ship-gate rules; the most specific matching rule wins (see Ship gates)
gatesmapNamed gate registry a ship_gates rule can reference by name (see Ship gates)
source_gateGateCoverage gate over the source authoring ladder (see source_gate)
verified_gateGateA single catch-all verified gate: the bar a locale must clear to read as verified, independent of shipping (see Ship gates)
verified_gatesShipGateRule[]Scoped verified-gate rules, in the same when/gate form as ship_gates; the most specific rule wins. With neither key set, no locale is verified

Project identity

kapi init writes an id: into every recipe it scaffolds:

version: v1
id: prj_k3m7xq2vw9ta5r8ybn4dcp
name: My App

The id is random, minted once, and derived from nothing else in the project. Everything kapi records about the project locally is keyed on it, so you can rename the project, move the folder, or clone the repository onto another machine and the recorded context still belongs to the same project. Two checkouts of one repository are one project because they share one recipe.

name: is the label you read in kapi status and in Kapi Desktop. Change it whenever you like.

Keep the id: line when you commit the recipe, and keep it when you copy a recipe as a starting point for a genuinely different project: delete the line there and run kapi init --mint-id in the new project, which writes a fresh id and leaves the rest of the file untouched.

A recipe with no id: keeps working. kapi identifies such a project by its name:, and kapi init --mint-id gives it an id whenever you want one.

Older kapi releases read a recipe carrying an id: without complaint: an unrecognized top-level key round-trips as an extension. Such a release still identifies the project by its name, so a project shared between two kapi versions is keyed differently by each until both carry this release.

Collections

collections is a list. Each entry is either a bare entry (one glob and its target, written inline) or a named collection, which groups content items under a name and can bind them to a point in the project's context space.

collections:
# Bare entry: a pattern and where it lands.
- path: "src/locales/en/*.json"
target: "src/locales/{lang}"

# Named collection: a name, the directory it lives in, its items.
- name: docs
channel: sdk/docs
base: web
content:
- path: "docs/**/*.mdx"
target: "i18n/{lang}/{path}.mdx"
FieldTypeDescription
namestringCollection name. Required to bind a channel:, and what a ship-gate rule's collections: selector matches
basestringThe directory this collection lives in; every path, target and item base below is written relative to it
channelstringThe point in the context space this collection's content sits at, always profile/channel (see Profiles and channels)
coordinatesmapDeclared axes (brand, mode) the collection sits at, overlaid per axis on defaults.coordinates; product and channel come from channel: and are rejected here (see Declared axes)
contentContentItem[]The collection's content items
source_languagestringOverrides defaults.source_language for this collection
target_languagesstring[]Overrides defaults.target_languages for this collection
source_onlyboolThe collection has no target language: kapi up skips it and target-coverage gates exclude it. Rejected if the collection or any item also carries a target
pathstringBare entries only: the glob, written instead of content:
formatstring or mapBare entries only
targetstringBare entries only

A named collection needs at least one item under content: and cannot also carry a path:; a bare entry needs a path:.

Set source_only: true on content kapi reads but never writes, such as package descriptions and installer strings. The flag changes no behaviour on its own, since a collection with no target was already source-only. It records the intent, and loading fails if the collection contradicts it. See Check-only content in other assets.

ContentItem

FieldTypeDescription
pathstringGlob pattern for source files, relative to the collection's base (required)
basestringDirectory a matched file's path is made relative to; defaults to the glob's fixed prefix
formatstring or mapFormat ID; auto-detected per file if omitted
targetstringOutput path template (see Source and target paths)
source_languagestringOverrides the collection's and the project's source language
target_languagesstring[]Overrides the collection's and the project's target languages
channelstringOverrides the collection's channel: for the files this item matches (see Profiles and channels)
redactionRedactionSpecOverrides defaults.redaction for this item
commentsbool or mapDeclares the comments in the matched files as content to check: Go source, read for its comments alone, and YAML, XML-based, HTML, Markdown, MDX, PO and properties formats, whose comments are checked beside the values their readers extract. The map form declares them too, and its directives list names markers your own tools read at the start of a comment line, which are set aside beside those under defaults.comments.directives. Its channel places the comments at a point of their own, a qualified profile/channel: each comment is checked under that point's voice and terms, and the rest of the file stays at the item's point. Its only: true declares the files for their comments alone: kapi checks the comments and reads none of the values, so kapi up, flow runs, kapi merge, kapi extract, kapi stats, kapi status coverage and the --ship gates leave the values alone, and the item cannot also set target, target_languages, redaction, format.config or format.preset. Go source is read for its comments alone with or without only. A format that supplies no comments makes the check report that it did not run (see Comments in source code)

base

A collection's base is where the collection lives. Every path, target and item base beneath it is written relative to that directory and joined onto it, so a collection reads as the tree it governs rather than as a prefix repeated on every line:

collections:
- name: docs
base: web
content:
- path: "docs/**/*.mdx" # → web/docs/**/*.mdx
target: "i18n/{lang}/{path}.mdx" # → web/i18n/{lang}/{path}.mdx

An item's own base is the separate, narrower thing: the directory a matched file's path is made relative to, which drives the {relpath} / {path} / {dir} tokens. An item that declares none keeps none, and the tokens then relativize against the joined pattern's own fixed prefix.

Source and target paths

path is a doublestar glob: ** matches across directories (e.g. src/**/*.json) and {a,b,c} matches any of the listed alternatives (e.g. input/store/*.{json,yaml,html}). One glob can therefore cover a whole directory of mixed content; the format is auto-detected per file unless you pin format. Patterns resolve relative to the project root. ** does not follow a symbolic link into another directory, so the linked packages a package manager keeps under node_modules stay out of a match. A link to a file that the pattern matches is read, and so is a linked directory that the pattern names before its first wildcard.

A file belongs to exactly one item. Items are matched in the order they are written, across all collections, and the first item whose path matches a file claims it; later items never see that file, however specific their pattern. An item declared comments: {only: true} claims only comments, wherever it is written: the first other item that matches a file claims its values, and the comments stay with the comments-only item unless that other item comes first and declares them itself. To place one file at its own point, or under its own format or target, list its item before the glob that would otherwise cover it:

collections:
- name: docs
channel: acme/docs
content:
- path: "docs/api-reference.md" # claimed here, at the reference point
channel: acme/reference
- path: "docs/**/*.md" # everything the item above did not claim

The base in play here is the item's own: the directory a matched file's path is made relative to, after any collection base has been folded in. It defaults to the glob's fixed prefix, the literal part of path before the first wildcard (input/docs/ for input/docs/**/*.md). The relative path drives the {relpath} / {path} / {dir} tokens and how much of the source tree a directory-mirror target reproduces.

target is the output-path template, expanded per source file and target language. The supported tokens are:

TokenMeaningExample (base input/, source input/docs/api.md)
{lang}Target languagefr
{relpath}Source path relative to base, with extensiondocs/api.md
{path}Source path relative to base, without extensiondocs/api
{dir}Directory portion of {relpath} (empty at the root)docs
{filename}Filename with extensionapi.md
{name}Filename without extension (alias {basename})api
{ext}Extension without the dotmd

A bare * is shorthand for {name}.

Directory-mirror (the easy default). When target ends with /, or its last segment has no extension and no wildcard/token, it names a directory and the source's {relpath} (under base) is mirrored beneath it. So target: output/{lang} reproduces the source tree under each per-language root (input/docs/api.mdoutput/fr/docs/api.md) with no * and no risk of a doubled extension:

collections:
- path: "input/**/*.md"
target: "output/{lang}"

Token template (the explicit form). When you need to reshape the layout, name the tokens:

collections:
- path: "src/locales/en/*.json"
target: "src/locales/{lang}/{name}.{ext}" # api.json → src/locales/fr/api.json

Profiles and channels

Content is written for a point in the context space. Two axes are structural: the product it belongs to and the channel it ships on. A key under profiles: is a product, the channels that profile lists are the channels that product ships on, and a collection names the point its content sits at with one channel: reference. Further axes are declared under coordinates: (see Declared axes). Context is the concept; this section is the schema.

profiles:
console:
channels: [app, docs]
voice: .kapi/profiles/console/voice.yaml
sdk:
channels:
- id: docs
concept: term:9a1c0f42b7
- cli
voice: .kapi/voice.yaml
valid_to: 2026-12-01

collections:
- name: console-app
channel: console/app
content:
- path: "console/i18n/**/*.kbf.json"
- name: sdk-docs
channel: sdk/docs
content:
- path: "web/docs/**/*.mdx"
- path: "web/docs/legal/**/*.mdx"
channel: sdk/cli
FieldTypeDescription
channelsChannel[]The surfaces this product ships on. A slug, or {id, concept} in long form
voiceVoiceBindingThe voice profile governing this product's content, in the same forms as defaults.voice. Unset keeps defaults.voice
termstorestringA standalone terms store governing this product's content, resolved relative to the project root. Unset, the project's own store governs
conceptstringThe concept that names this product, for display. Carried and shape-checked, never resolved during matching
valid_fromstringDate (YYYY-MM-DD) or RFC3339 instant this profile starts governing, inclusive. Unset is unbounded (see Profile validity)
valid_tostringDate or RFC3339 instant it stops, exclusive. Unset is unbounded

The map key is the profile's name: the product-axis value its collections carry, and the directory under .kapi/profiles/<name>/ holding the files it overrides. A profile that binds neither a voice nor a vocabulary is still a profile: the directory is the binding.

Profile names and channels are slugs: lowercase letters, digits and hyphens, starting with a letter or digit. They are machine identifiers (stable, never translated, comparable byte for byte), which is why a profile or a channel may carry a concept for display but resolution never looks at it.

A collection's channel: is always written profile/channel: the product owns the surface, and the binding reads as one. A bare channel name is a load error that spells out the qualified form(s), even when exactly one profile declares it. Binding a channel requires a named collection. A collection that binds none is governed by the project default point, defaults.voice and the project's own terms.

A content item may carry its own channel:, in the same qualified form and under the same load-time validation, for the files that item matches. It is the finest point a recipe can name, and everything resolves to it: a flow run partitions those files out of the rest of the collection and gives them their own voice and terms, kapi check gates them against that vocabulary, and kapi context search reports it as the point each use sits at.

On a connected project, the resolved product and channel travel to the server as the content's coordinates, so both venues resolve the same voice for the same content. A profile's termstore: does not: it is a path into the local project, and a connected run warns that the binding applies locally only.

Conventional files

A profile's own directory answers before the project default does, so overrides kept in the conventional place need no line in the recipe. Resolution for a collection's point, most specific first:

StoreOrder
voiceprofiles.<name>.voice.kapi/profiles/<name>/voice.yamldefaults.voice.kapi/voice.yamlvoice.yaml
terms--termstoreprofiles.<name>.termstore.kapi/profiles/<name>/terms.json (when the file exists) → the project's own store

The channel then refines the voice that resolved: a matching entry under the voice profile's own channels: replaces its tone, its style, or both, and can add vocabulary rules. The profile's own vocabulary applies on every channel, and a channel's rules only add to it.

Declared axes

Beyond product and channel, a project names the further axes its content varies along under defaults.coordinates. Every collection inherits them; a collection that sits elsewhere on one axis sets that axis in its own coordinates: and keeps the rest. Per axis, the most specific value wins.

defaults:
coordinates:
brand: acme

collections:
- name: sdk-docs
channel: sdk/docs
coordinates:
mode: reference
content:
- path: "web/docs/**/*.mdx"
AxisConventional valuesMeaning
branda slugThe brand a point sits under, coarser than product
modetutorial, how-to, reference, explanationThe kind of document at the point, in the Diátaxis sense

The map is open: any other axis name is accepted, and the values are conventions rather than an enumeration. product and channel are refused under coordinates: at load, because both are derived from channel: and a declared copy could contradict the derived one. A collection's resolved point (the defaults, overlaid with what channel: derives, overlaid with its own coordinates:) is what kapi context reports and what a connected project sends to the server.

Profile validity

valid_from and valid_to bound a profile's governance in time, the same half-open window a term carries, with valid_from inclusive and valid_to exclusive, so a valid_to date excludes that whole day. A bound is a date (YYYY-MM-DD) or an RFC3339 instant; an unparseable bound or an inverted range fails at load.

A profile outside its window does not govern. Resolution falls through to the next binding (a collection's channel under an expired item channel, then the project default) exactly as if the expired profile were absent, and the run, check or push that applied the fall-through reports it once:

governance: profile "sdk" expired 2026-12-01; governing with the project default

The instant is the run's own wall clock; there is no flag to resolve for another date. kapi context search reads every bounded profile against the current time and labels it active, upcoming or expired, so a window can be seen before it closes.

Defaults

defaults holds project-wide processing settings that individual content items can override. Several of these are governed by M-01.

FieldTypeDescription
source_languagestringBCP-47 source locale (e.g., en)
target_languagesstring[]BCP-47 target locales
locale_formatstringLocale style for the {lang} placeholder: bcp-47 (default) or posix
concurrencyintNumber of files to process in parallel
parallel_blocksintNumber of blocks to process in parallel within a file
encodingstringInput file encoding (default: utf-8)
excludestring[]Glob patterns no collection claims: content scanning skips them, and a file you name that one matches sits at the project's default point (e.g. **/*.generated.json)
formatsmapPer-format preset, config, and detection priority overrides (see formats)
mergeMergeDefaultsHow kapi merge resolves a translator's target against an existing target or content-memory entry (see merge)
memoryMemoryDefaultsContent-memory pre-fill on kapi extract and write-back on kapi merge (see memory)
segmentationSegmentationDefaultsOpt-in sentence-level segmentation overlay applied on extract (see segmentation)
redactionRedactionSpecReplace sensitive content with protected placeholders before processing and restore it afterwards (see redaction)
annotationsAnnotationDefaultswrite: lists the annotation types a writer draws into the document as inline marks, out of what the format declares it can carry. Empty draws every type the format supports; naming a type a format cannot carry asks for nothing there
commentsmapdirectives: markers your own tools read at the start of a comment line, set aside wherever kapi reads the project's comments. channel: the point, a qualified profile/channel, at which every declared item's comments sit unless the item names its own (see Comments in source code)
voiceVoiceBindingBind a voice profile as standing project context (see voice). A profile's own voice: overrides it for that product's content
coordinatesmapThe project's default point on the declared axes (brand, mode), inherited by every collection; product and channel are derived from channel: and rejected here (see Declared axes)
terms_sourcestringCommitted, git-tracked native source bundle (a .terms.json) the project's terms are compiled from; kapi apply edits it and re-imports into the project store, so git diff is the review surface. Binds any path. Unset, kapi falls back to <root>/.kapi/terms.json (the conventional home in the committed context graph), then <root>/terms.json
memory_sourcestringCommitted, git-tracked native source bundle (a .memory.json) the project content memory is compiled from, the content-memory analogue of terms_source. It has no conventional-location fallback: a project has one approved term list but many memory bundles (one per content surface), so a seed is always named
toolsmapProject-level tool presets, keyed by tool name: config applied wherever that tool runs in a project flow. A step's own config overrides a preset per key. term_rules: is the key term-check, translate, recycle, dnt-check and pseudo-translate take here (one term, what to use instead, and a severity; see the voice profile reference)
localesmapPer-target-language overrides keyed by locale. Each entry's tools: presets merge over defaults.tools and under a step's own config whenever a flow runs for that locale, so one locale can enable redaction or a different model without forking the flow (see the kapi loop)
flowstringThe project's default flow, the composed flow kapi up runs to catch up every locale. A built-in name (e.g. translate) or a key in flows:. Empty runs the built-in default (recycle → translate)
materializestringWho delivers translated files. manual (default) leaves delivery to kapi merge or kapi up --materialize; on-converge makes kapi up write a locale's files to the collection's target: only once its gated scopes are shippable
jobsintHow many target languages one kapi up pass runs concurrently. 0 (default) leaves it to the runner (4); --jobs overrides per run
source_gatestringSource-first convergence level: the source status a block must reach before it is translated (see source_gate)

Individual content items can override redaction per entry (ContentItem.redaction).

source_gate

defaults.source_gate is the source-first convergence gate: the source status a block must reach before the loop translates it. It gates the fan-out, and is distinct from the top-level source_gate coverage bar that kapi check --ship evaluates. Source-first convergence settles the source (terminology, voice, source checks) and holds the fan-out on it, so an unsettled, non-compliant, un-term-checked source is never translated into every locale only to be redone when it changes. See Convergence: source first.

defaults:
source_gate: checked # the default: source cleared its automated terminology,
# voice, and source checks (no human bottleneck)
# source_gate: approved # require a human/agent source sign-off first
# source_gate: none # opt out: raw fan-out, no source gate
ValueMeaning
(unset)Runner applies the default (checked)
authoredPresence baseline: any non-empty source qualifies
checkedDefault: source passed its automated terminology, voice and source checks
approvedA human or agent signed off the source (voice-critical or regulated content)
noneDeliberate opt-out: translate without settling the source first

On a connected project, a below-gate source holds the convergence run (source_not_ready) and opens a source-review task rather than translating; the source-side coverage bar and this convergence level are independent controls.

formats

defaults.formats configures formats by name, so a single wildcard content item (path: "input/*", no format:) can let each file's engine be auto-detected while you still tune individual formats in one place. Each entry takes:

FieldTypeDescription
presetstringA named format preset applied to this format (see kapi formats)
configmapReader option overrides for this format (merged under, and overridable by, a content item's format.config)
priorityintDetection priority; higher wins when several formats claim the same extension

priority is the engine-pin escape hatch. Most extensions map to one format, but some are claimed by several: .srt by both okf_vtt and okf_regex, .txt by several plain-text engines. Bumping the preferred one steers auto-detection without naming a format on every item:

plugins:
okapi-bridge: "*"
defaults:
formats:
okf_vtt:
priority: 110 # .srt → WebVTT engine (beats okf_regex)
okf_json:
config:
useFullKeyPath: true
collections:
- name: Source Files
content:
- path: "input/*" # every file; engine auto-detected, two pinned above
target: "output/{lang}"

Plugin formats already outrank built-in ones by default (priority 100 vs 50), so declaring okapi-bridge is enough for input/* to resolve to the okf_* engines; priority only matters to break ties between formats from the same source.

merge

FieldTypeDescription
conflict_policystringtranslator-wins (default), existing-wins, or newest-wins

memory

FieldTypeDescription
fuzzy_thresholdintMinimum fuzzy match score (0–100) to pre-fill the target on extract (default: 75)

segmentation

FieldTypeDescription
sourceboolToggle sentence-level segmentation of source text on extract
srxstringOptional SRX rules file; built-in default rules are used when empty

redaction

The sensitive term list lives in a separate rules file referenced by rules (so it can be gitignored); this spec points at it and selects detection backends.

FieldTypeDescription
enabledboolTurn redaction on for extract/merge
rulesstringPath to a redaction rules YAML file
detectorsstring[]Detection backends: rules and/or entities
placeholderstringOverride the visible stand-in template, e.g. [REDACTED:{category}]

voice

Bind a voice profile under defaults.voice. Exactly one source is expected. The short form is the profile file itself, voice: .kapi/voice.yaml, which is what a recipe writes when the profile is a file in the project, as it usually is.

FieldTypeDescription
profile_filestringPath to a standalone profile YAML, resolved relative to the project root
profilestringName of a profile in the local voice store
packstringName of a built-in starter pack

Flow Steps

Each flow contains an ordered list of steps. See Flow Steps Format for the full specification.

flows:
my-flow:
steps:
- tool: translate # tool name (required)
config: # tool-specific config (optional)
provider: anthropic
label: "Translate" # display label (optional)

Steps can also define parallel branches:

flows:
parallel-qa:
steps:
- parallel:
- tool: qa
- tool: qa
config:
provider: anthropic

Ship gates

Ship gates decide when translated content is shippable: coverage thresholds over the target lifecycle ladder (drafttranslatedreviewedsigned-off), enforced by kapi check --ship and driven toward by kapi up. The model (ladders, derived state, review) is the kapi loop; this section is the schema. All of these keys are top-level (siblings of defaults) and optional: with none set, nothing is gated.

FieldTypeDescription
ship_gateGateA single catch-all gate: state name → threshold
ship_gatesShipGateRule[]Scoped rules, each a selector plus a gate; when present it replaces ship_gate
gatesmapNamed gate registry; a ship_gates rule references an entry with gate: <name>
source_gateGateA gate over the source authoring ladder (authoredcheckedapproved)
verified_gateGateA single catch-all verified gate, in the same threshold form as ship_gate
verified_gatesShipGateRule[]Scoped verified-gate rules; the most specific rule wins

Shipping and being verified are independent readings. A verified gate names the bar a locale must clear to be reported as verified (by kapi status --ship and the ship-aware language picker), and a locale can ship without meeting it. With neither verified key set, no locale is verified.

A scope no ship gate rule matches is not gated. kapi status and kapi up name it not gated rather than shippable, and kapi status --ship records "state": "not_gated" for its locale. Nothing holds a not-gated scope back on coverage, so kapi up delivers it under materialize: on-converge and a language picker offers it. Stale wording, a translation a reviewer turned down, a failing check, or terms that were not checked withhold a scope whether or not a gate matches it. Each scope reads as one of three ship states:

StateWhen
shippableA ship gate matches the scope and the scope clears it, with nothing withholding it
withheldThe scope is short of a matching gate, or stale, rejected, failing or unchecked content holds it back
not_gatedNo ship gate matches the scope, and nothing withholds it

Gate and thresholds

A gate maps a lifecycle state to a threshold. A scope clears the gate when, for every threshold, at least that percent of its units are at that state or higher on the ladder. A threshold is written in one of two forms:

FormExampleMeaning
Shortreviewed: 80At least 80% at reviewed or higher; approver class defaults to human
Extendedreviewed: {pct: 80, by: any}pct (0–100) plus an explicit approver class by

pct must be 0–100; a threshold of 0 means "not required". by selects the approver class for the decision rungs (reviewed, signed-off): human (the default) counts only decisions made by a person or an agent acting for one, any also counts autonomous AI approvals. The short form always means by: human; an AI-approved unit never satisfies it. See approver classes for the rationale.

Simple form, one bar for the whole project:

ship_gate: { translated: 100, reviewed: 80 }

Every unit translated, the important 80% reviewed by a person; the long tail ships machine-translated.

Extended thresholds, the same shape with explicit pct/by:

ship_gate:
translated: 100
reviewed: { pct: 100, by: any }

Fully reviewed, but autonomous AI reviews count toward the bar.

ShipGateRule

ship_gates is an ordered rule list. Each rule has:

FieldTypeDescription
whenSelectorOptional match: locales and/or collections (string lists). Omitted = catch-all default
gateGate or stringAn inline threshold map, or the name of an entry in the gates registry

An empty selector axis matches everything, so a rule with no when: is the default. When several rules match a (collection, locale) unit, the rule constraining the most axes wins wholesale; among equal specificity, the earliest-listed wins.

Scoped rules, where different scopes answer to different bars:

ship_gates:
- when: { locales: [de] } # contractually reviewed market
gate: { translated: 100, reviewed: 100 }
- when: { collections: [Legal] } # regulated content, every locale
gate: { translated: 100, signed-off: 100 }
- gate: { translated: 100, reviewed: 80 } # the default

Named gates, defined once in the gates registry and referenced by name:

gates:
contractual: { translated: 100, reviewed: { pct: 100, by: human } }
machine: { translated: 100, reviewed: { pct: 100, by: any } }

ship_gates:
- when: { locales: [de, ja] }
gate: contractual
- gate: machine

A tiered per-market strategy built from these pieces is worked through in Tier gates per market.

source_gate

The source-side coverage gate: a threshold map over the authoring ladder (authoredcheckedapproved), gating the author's own content rather than the translations.

source_gate: { checked: 100 }

Every source unit clears its bound voice and terminology checks before kapi check --ship passes. Like ship_gate, the top-level source_gate is a coverage bar evaluated by kapi check --ship; source drift never blocks an ordinary build.

This is distinct from defaults.source_gate, the source-first convergence level (checked / approved / none) that decides whether the loop translates a block at all. The coverage bar answers is the source shippable?; the convergence level answers may this source be translated yet?.

Key Properties

  • No credentials: API keys are never stored in a kapi project file. They come from the OS keychain (kapi credentials add, shared with Kapi Desktop) or from environment variables.
  • No state: no sync cursors, caches, or timestamps. A kapi project file is always clean and safe to commit.
  • Portable: one kapi.yaml per project folder; share via git.
  • CLI-compatible: kapi run flowname -p kapi.yaml

Using with Kapi CLI

# Run a flow from a Kapi project
kapi run translate -p kapi.yaml

# Override defaults with CLI flags
kapi run translate -p kapi.yaml --target-lang de --provider openai

# One-shot mode still works without a project
kapi translate -i file.json --target-lang fr

Using with Kapi Desktop

  • File > Open loads a project by its folder (the one containing kapi.yaml)
  • File > New Project creates one from scratch: a name, a location and a source language
  • File > Recent Projects reopens one you have worked in before

The app opens to your projects and edits the recipe in place: content and governance under Project Settings, the stores under Context. See the desktop overview.

See also

  • C-01: Project model: the architectural decision behind the kapi.yaml recipe and its extension mechanism.
  • kapi.yaml Project File Format: the implementation note; the KapiProject, Defaults, Collection and Profile struct layouts, the named-collection content shape, validation rules, and how platform extensions decode unknown keys.
  • Flow Steps Format: the full step specification used by flows.