Project file
A Kapi project is a portable kapi.yaml recipe at a project's root that captures a content workflow: source and target languages, content file patterns, 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
name: My App
content:
- 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
tm:
fuzzy_threshold: 75
segmentation:
source: true
termbase: terms/product.db
Fields
Required
| Field | Type | Description |
|---|---|---|
version | string | Schema version, must be "v1" |
name | string | Project display name |
Optional
| Field | Type | Description |
|---|---|---|
content | ContentEntry[] | File patterns to process (see ContentEntry) |
preset | string | A framework preset name (see kapi init --list-presets) (e.g., nextjs, react-intl) applied to formats |
plugins | map | Plugin requirements keyed by name (e.g., okapi: "^1.47.0") |
requires | map | Plugin name → semver constraint that gates loading; the recipe refuses to load without a registered plugin |
flows | map | Named flow definitions (see Flow Steps) |
defaults | Defaults | Processing defaults (see Defaults; source/target languages live here) |
ship_gate | Gate | A single catch-all ship gate (see Ship gates) |
ship_gates | ShipGateRule[] | Scoped ship-gate rules; the most specific matching rule wins (see Ship gates) |
gates | map | Named gate registry a ship_gates rule can reference by name (see Ship gates) |
source_gate | Gate | Coverage gate over the source authoring ladder (see source_gate) |
ContentEntry
| Field | Type | Description |
|---|---|---|
path | string | Glob pattern for source files (required) |
base | string | Directory a matched file's path is made relative to; defaults to the glob's fixed prefix |
format | string | Format ID; auto-detected per file if omitted |
target | string | Output path template (see Source and target paths) |
In a named collection, base may be set once on the collection and is inherited
by every item that does not set its own.
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.
base is the directory a matched file's path is made relative to. 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:
| Token | Meaning | Example (base input/, source input/docs/api.md) |
|---|---|---|
{lang} | Target language | fr |
{relpath} | Source path relative to base, with extension | docs/api.md |
{path} | Source path relative to base, without extension | docs/api |
{dir} | Directory portion of {relpath} (empty at the root) | docs |
{filename} | Filename with extension | api.md |
{name} | Filename without extension (alias {basename}) | api |
{ext} | Extension without the dot | md |
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.md → output/fr/docs/api.md — with no * and no risk of a
doubled extension:
content:
- name: Docs
base: input
items:
- path: "input/**/*.md"
target: "output/{lang}"
Token template (the powerful form). When you need to reshape the layout, use tokens explicitly:
content:
- path: "src/locales/en/*.json"
target: "src/locales/{lang}/{name}.{ext}" # api.json → src/locales/fr/api.json
Defaults
defaults holds project-wide processing settings that individual content items can override. Several of these are governed by AD-017.
| Field | Type | Description |
|---|---|---|
source_language | string | BCP-47 source locale (e.g., en) |
target_languages | string[] | BCP-47 target locales |
locale_format | string | Locale style for the {lang} placeholder: bcp-47 (default) or posix |
concurrency | int | Number of files to process in parallel |
parallel_blocks | int | Number of blocks to process in parallel within a file |
encoding | string | Input file encoding (default: utf-8) |
exclude | string[] | Glob patterns skipped during content scanning (e.g. **/*.generated.json) |
formats | map | Per-format preset, config, and detection priority overrides (see formats) |
merge | MergeDefaults | How kapi merge resolves a translator's target against an existing target or content-memory entry (see merge) |
tm | MemoryDefaults | Content-memory pre-fill on kapi extract and write-back on kapi merge (see tm) |
segmentation | SegmentationDefaults | Opt-in sentence-level segmentation overlay applied on extract (see segmentation) |
redaction | RedactionSpec | Replace sensitive content with protected placeholders before processing and restore it afterwards (see redaction) |
brand_voice | BrandVoiceBinding | Bind a brand voice profile as standing project context (see brand_voice) |
termbase | string | Path to a terms store, resolved relative to the project root; used for project-scoped term enforcement with no --termbase flag |
termbase_source | string | Committed, git-tracked native source bundle (a .terms.json) the project terms store is compiled from; kapi apply edits it and re-imports into the gitignored terms cache, so git diff is the review surface. Unset, kapi falls back to <root>/terms.json, then <root>/.kapi/terms.json |
tm_source | string | Committed, git-tracked native source bundle (a .memory.json) the project content memory is compiled from — the content-memory analogue of termbase_source. It has no conventional-location fallback: a project has one glossary but many memory bundles (one per content surface), so a seed is always named |
state | string | Committed, git-tracked project state store (a kapi-project-state JSON document) — the authoritative carrier of per-unit workflow decisions (review ladder, approvals, parking) a plain target file can't hold. Empty defaults to .kapi-state.json; a server-backed project pushes state to a remote instead |
flow | string | The 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) |
jobs | int | How many target languages one kapi up pass runs concurrently. 0 (default) leaves it to the runner (4); --jobs overrides per run |
source_gate | string | Source-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 —
distinct from the top-level source_gate coverage bar that
kapi check --ship evaluates. Source-first convergence settles the source
(terminology, brand, source QA) and holds the fan-out on it, so an unsettled,
off-brand, 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,
# brand, and source-QA 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
| Value | Meaning |
|---|---|
| (unset) | Runner applies the default (checked) |
authored | Presence baseline — any non-empty source qualifies |
checked | Default — source passed its automated terminology, brand, source-QA checks |
approved | A human or agent signed off the source (brand-critical / regulated) |
none | Deliberate 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:
| Field | Type | Description |
|---|---|---|
preset | string | A named format preset applied to this format (see kapi formats) |
config | map | Reader option overrides for this format (merged under, and overridable by, a content item's format.config) |
priority | int | Detection 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
content:
- name: Source Files
base: input
items:
- 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
| Field | Type | Description |
|---|---|---|
conflict_policy | string | translator-wins (default), existing-wins, or newest-wins |
tm
| Field | Type | Description |
|---|---|---|
fuzzy_threshold | int | Minimum fuzzy match score (0–100) to pre-fill the target on extract (default: 75) |
read | string[] | Additional read-only content-memory files consulted during pre-fill; writes always go to the project content memory |
segmentation
| Field | Type | Description |
|---|---|---|
source | bool | Toggle sentence-level segmentation of source text on extract |
srx | string | Optional 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.
| Field | Type | Description |
|---|---|---|
enabled | bool | Turn redaction on for extract/merge |
rules | string | Path to a redaction rules YAML file |
detectors | string[] | Detection backends: rules and/or entities |
placeholder | string | Override the visible stand-in template, e.g. [REDACTED:{category}] |
brand_voice
Bind a brand voice profile under defaults.brand_voice. Exactly one source is expected.
| Field | Type | Description |
|---|---|---|
profile_file | string | Path to a standalone profile YAML, resolved relative to the project root |
profile | string | Name of a profile in the local brand store |
pack | string | Name 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 (draft → translated → reviewed →
signed-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 four keys are top-level (siblings of defaults) and optional:
with none set, nothing is gated.
| Field | Type | Description |
|---|---|---|
ship_gate | Gate | A single catch-all gate: state name → threshold |
ship_gates | ShipGateRule[] | Scoped rules, each a selector plus a gate; when present it replaces ship_gate |
gates | map | Named gate registry; a ship_gates rule references an entry with gate: <name> |
source_gate | Gate | A gate over the source authoring ladder (authored → checked → approved) |
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:
| Form | Example | Meaning |
|---|---|---|
| Short | reviewed: 80 | At least 80% at reviewed or higher; approver class defaults to human |
| Extended | reviewed: {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 review decisions count toward the bar.
ShipGateRule
ship_gates is an ordered rule list. Each rule has:
| Field | Type | Description |
|---|---|---|
when | Selector | Optional match: locales and/or collections (string lists). Omitted = catch-all default |
gate | Gate or string | An 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 — 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 — define once in the gates registry, reference 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
(authored → checked → approved), gating the author's own content rather
than the translations.
source_gate: { checked: 100 }
Every source unit clears its bound brand 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 Kapi project files. They come from the OS keychain (Kapi Desktop) or environment variables (CLI).
- No state — No sync cursors, caches, or timestamps. Kapi project files are always clean and safe to commit.
- Portable — One
kapi.yamlper 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 to load a project by its folder (the one containing
kapi.yaml) - File > New to create one from scratch
- File > Save / Save As for standard document operations
- Open a project by its folder rather than by double-clicking a branded file
- Drag and drop a project folder onto the app window
See also
- AD-008: Project model — the architectural decision behind the
kapi.yamlrecipe and its extension mechanism. - kapi.yaml Project File Format — the implementation note: the
KapiProject,Defaults, andContentCollectionstruct 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.