Skip to main content

YAML Flow Authoring

A flow is a YAML file that lists the tools a run applies, in order. You write it as a list of steps, and kapi compiles the steps into a graph of tool nodes and edges for execution.

Steps format​

A flow is a list of sequential steps. Each step references a tool by name and optionally provides configuration:

steps:
- tool: pseudo-translate
config:
expansionPercent: 30
prefix: "["
suffix: "]"

The run supplies the target locale to every step (--target-lang at the CLI, or the project's declared targets under kapi run / kapi up), so one flow serves every locale; a targetLocale written into a step's config is outranked by the run's language.

Source and sink​

A flow carries only its steps. Where content enters and leaves is set by bindings that the run resolves: a file, the project store, a .kpz workspace, an interchange import/export, or none. A flow declares a binding only when it is intrinsic to the flow (e.g. a check flow that produces no document sets sink: none), and never a path:

sink: none # only when intrinsic; otherwise omit it and the run sets it
steps:
- tool: qa

At the CLI the binding comes from -i / -o (a plain path is detected, a scheme: is explicit), the project / .kpz you are in, or auto-detection; kapi run <flow> --explain shows the resolved source → sink. See E-04: Flow I/O Binding.

Step labels​

Add a label for readability in the UI graph view:

steps:
- tool: pseudo-translate
label: Generate test translations
config:
expansionPercent: 30

Sequential steps​

Steps execute in order. The output channel of one tool feeds into the input channel of the next:

steps:
- tool: create-target
config:
copySource: true

- tool: search-replace
config:
pairs:
- search: "TODO"
replace: ""
target: true

- tool: qa

This creates a three-step pipeline: create the target, clean up placeholder text, then run quality checks.

Steps run in order​

A flow runs its steps as one ordered chain: each tool reads the Parts the step before it emitted. To run several checks, list them one after another:

steps:
- tool: create-target
config:
copySource: true

- tool: qa
label: Quality checks
- tool: term-check
label: Terminology checks
- tool: xml-validation
label: XML validation

Each tool runs in its own goroutine, so the checks work on different Parts at the same time. Within one tool, --parallel-blocks (or a recipe's parallel_blocks) spreads blocks across workers for a tool built as a plain tool.BaseTool with a block handler. The AI and MT tools keep their own processing, described in Parallel block processing. The steps parser also accepts a parallel: block, but no runner executes one: kapi run stops with an error before processing a flow that contains it.

Transformers​

Tools that rewrite the source (redaction, whitespace/markup normalization, a source-mutating script) are ordinary steps in the same ordered list as everything else. A transformer returns an edit plan; the framework applier performs the rewrite inline and in order, rebasing surviving run-anchored overlays (segmentation, terms) onto the new runs, so each transformer settles the source before later steps observe it.

steps:
- tool: redact
config:
detectors: [rules]
rulesPath: redaction-rules.yaml

- tool: translate

A placement pass (core/flow/placement.go) validates transformer positions beside data-flow validation at every flow build and load:

  • Error: a transformer follows a step that produces a committed target (rewriting source orphans the targets). A transformer that produces targets itself, such as unredact, is exempt.
  • Error: a recoverable transformer (redact) follows a step with the remote-source-egress or process-exec side effect, except a step producing an input the transformer's config-resolved contract requires (a cloud NER step feeding entity-driven redaction is the documented detection trade-off, C-10). AI tools configured with a local provider (ollama, demo) carry no egress effect.
  • Warning: a transformer placed later than its earliest valid slot, since every overlay present at apply time must be rebased.

A flow that declares the removed source_transforms: field is rejected at load with a migration error directing you to list the transformers as ordered steps. See the tool-system AD for the immutability model and the producer/applier split.

How steps compile to the graph​

The StepsToGraph() function transforms a StepsSpec into FlowNode and FlowEdge slices:

  1. Each sequential step becomes a tool node, chained by edges
  2. A parallel: block creates multiple tool nodes, all connected from the previous node, and the step after it connects from every branch; this shape exists in the definition only and cannot run

The graph holds tool nodes only. The run supplies the flow's source and sink as bindings (E-04).

The host builds the ordered tool chain from the steps, and the Executor runs it: each tool becomes a goroutine connected to the next by a buffered channel.

Example flows​

Translation pipeline​

A typical translation flow with content-memory leverage, AI translation for new blocks, and quality checks:

steps:
- tool: create-target
config:
copySource: false

- tool: recycle
label: Apply memory matches
config:
fuzzyThreshold: 75

- tool: translate
label: Translate remaining
config:
provider: anthropic

- tool: qa
label: Quality checks

Run it for each locale you ship:

kapi run my-translate -i input.xliff --target-lang fr
kapi run my-translate -i input.xliff --target-lang de

Checks after pseudo-translation​

Run analysis tools over pseudo-translated output:

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

- tool: term-check
- tool: qa
config:
checkAbsoluteMaxCharLength: true
absoluteMaxCharLength: 200
kapi run pseudo-checks -i input.json --target-lang qps

Script filtering​

Use the JavaScript script step to filter or transform parts programmatically:

steps:
- tool: script
label: Skip short blocks
config:
code: |
if (part.type === 'block') {
var text = part.block.source[0].content.text;
if (text.length < 3) {
skip();
}
}

- tool: pseudo-translate

Running flows​

From the CLI​

# Run a built-in composed flow
kapi run translate-qa -i input.xliff --target-lang fr

# Run a flow defined in a kapi.yaml project file
kapi run my-flow -p kapi.yaml -i input.json --target-lang fr

# List available flows
kapi flows

# Write no file; print the change set a pass of the project would apply
kapi up -p kapi.yaml --print-ops > change.json
kapi apply -p kapi.yaml change.json

A run that writes a file commits each document through the file home a change set commits through: the destination is staged beside the file and renamed onto it under the file's lock, only while the file still holds what it held when the run began, so a file a person saved while the run worked keeps their bytes. In a project each document the run wrote is recorded as the flow's edit, with the source each translation was made from. --print-ops writes no file and prints the operations the run would apply, addressed to the document and the edition the run changed; kapi apply applies them to the same bytes. Under materialize: on-converge a translation whose file does not exist yet lives in the workspace home, so kapi apply keeps it there and kapi merge writes its file. Every file left out of the change set is named on stderr, among them a target file the run writes whole from its source where kapi apply would write other bytes, and a file the change service does not keep the edition in (an -o path rather than the recipe's target). In a project, kapi run without -o writes no file, so it prints nothing. See E-09: The change contract.

Programmatically​

An ad-hoc programmatic run has no --target-lang to inherit, so the locale is passed in each step's config:

spec := &flow.StepsSpec{
Input: "json",
Steps: []flow.FlowStep{
{Tool: "pseudo-translate", Config: map[string]any{
"targetLocale": "fr",
"expansionPercent": 30,
}},
{Tool: "qa", Config: map[string]any{
"targetLocale": "fr",
}},
},
}

nodes, edges, err := flow.StepsToGraph(spec)
// Build and execute with Executor...