Skip to content
Talk to our solutions team

The flow file

A flow file is YAML. One key is required — tasks: — and everything else exists to feed it.

name: build-and-ship
list: true
vars:
registry: registry.internal
tasks:
- name: build
shell: docker build -t {{registry}}/app:latest .
- name: push
shell: docker push {{registry}}/app:latest

Note list: true on the second line. It is not decoration — read the next section before writing anything with more than one node.

A flow runs in one of two modes, and the difference decides whether your second node runs at all.

By default a flow is a graph. Nodes are connected by their next: keys and by nothing else. A node without next: is terminal:

name: three-steps
tasks:
- name: one
shell: echo one
- name: two
shell: echo two
- name: three
shell: echo three
one
execution engine instance Status: completed Completed Nodes: [one]

The run stops after one and reports completed. Writing nodes underneath one another looks like a sequence; in graph mode it is three unconnected terminal nodes, and the engine runs the first.

Two ways to get all three:

# Explicit edges — required as soon as there is any branching
tasks:
- name: one
shell: echo one
next:
go: two
- name: two
shell: echo two
next:
go: three
- name: three
shell: echo three
# A declared sequence — no routing to write
list: true
tasks:
- name: one
shell: echo one
- name: two
shell: echo two
- name: three
shell: echo three

Use list: true when the flow is a straight line. It says what you mean, it cannot be got wrong by omission, and it removes the routing boilerplate. Use graph mode the moment you need to branch, fan out, retry to a different node, or handle an error somewhere other than the end.

The two modes also fail differently:

Graph (default)list: true
OrderWhatever next: saysTop to bottom
A node with no next:Ends the runContinues to the next
On failureFollows error:, or stopsStops
continueonerror defaulttruefalse

In either mode a run that hit a failure is reported as errored even if a recovery branch succeeded afterwards. See Execution.

KeyTypePurpose
namestringThe flow’s name. Used to select it with -n when a file holds several
taskslistThe nodes. See Node types
varsmapVariables available to every node as {{name}}
tablesmapNamed row sets, for data-driven runs
tablestringWhich table to iterate — runs the whole flow once per row
importsmapShort aliases for file URIs
flowslistSeveral flows in one file
stateslistNodes of a state machine, instead of tasks:
starttaskstringWhich node runs first. Defaults to the first in the list
affinitymapPin the run to one worker. See Execution
continueonerrorboolWhether a failed node stops the run
listboolRun the nodes top to bottom instead of following next:. See above
pipelineboolRun as a streaming data pipeline

vars: is the flow’s starting state. Values can be scalars, lists or nested maps:

vars:
environment: staging
replicas: 3
registry:
host: registry.internal
namespace: platform

Read them with {{ }}, using dots for nesting:

tasks:
- name: deploy
shell: |
helm upgrade app ./chart \
--set image.repository={{registry.host}}/{{registry.namespace}}/app \
--set replicaCount={{replicas}} \
--namespace {{environment}}
Terminal window
kis flow -f deploy.yaml -v environment=production -v replicas=6
kis flow -f deploy.yaml -e production.yaml

-v sets one variable and repeats. -e loads a YAML file of them, which is the usual way to keep per-environment values out of the flow.

Two mechanisms, and the difference matters.

setvar: captures what an atom produced:

list: true
tasks:
- name: version
shell:
script: git describe --tags
setvar: release_tag
- name: tag-image
shell: docker tag app:latest app:{{release_tag}}

assign: sets values directly, without running an atom:

tasks:
- name: defaults
assign:
set:
region: us-west-2
retries: 3
next:
go: deploy

Use setvar: for the result of work, assign: for constants and reshaping. An assign: node never reaches a worker, so it costs nothing.

Some names are populated for you. They all begin with an underscore.

VariableSet by
_itemThe current item, inside foreach: or map:
_indexThe current item’s position, from zero
_resultsEvery result, once a foreach: or map: finishes
_resultThe last node’s result
_error, _causeA fail: node’s error type and message
_signalThe data a wait: node received
_wait_timeouttrue when a wait: node timed out

A table is a named set of rows. Declare one inline as pipe-separated text, with the first line as the header:

tables:
servers: |
host|role
web-1|frontend
db-1|database

Or load one from a file:

tables:
servers:
type: csv
file: ./data/servers.csv
separator: ","
inventory:
type: excel
file: ./data/inventory.xlsx
sheet: Sheet1

Naming a table with the top-level table: key turns the whole flow into a data-driven run: it executes once for every row, with that row’s columns available as variables.

name: check-servers
vars:
greeting: checking
tables:
servers: |
host|role
web-1|frontend
db-1|database
table: servers
tasks:
- name: show
shell: "echo {{greeting}} {{host}} is {{role}}"
checking web-1 is frontend
checking db-1 is database

Each row gets a complete, independent run — its own variables, its own record of which nodes completed. That is the difference between this and a map: node: map: fans one node out over a collection, table: fans the whole flow out over rows.

Variables from vars: are merged into every row, so {{greeting}} above is available alongside the row’s own columns. A column with the same name as a variable wins for that row.

imports: binds short names to file URIs, so a path used in several places is written once:

imports:
selector: ./scripts/select-candidates.js
hosts: ./data/production-hosts.csv
tables:
servers:
type: csv
file: hosts
tasks:
- name: pick
script:
language: javascript
file: selector
setvar: candidates

Aliases work anywhere a file URI is accepted — a script’s file:, a table’s file:, a subflow reference. Paths are resolved against the product root when the flow sits inside one, and against the flow file’s own directory otherwise.

flows: holds a list, each with its own name: and tasks:. Select one with -n:

flows:
- name: deploy-service
tasks:
- name: run
shell: "./deploy.sh {{service_name}}"
- name: done
succeed: true
- name: deploy-all
tasks:
- name: each-service
foreach:
items_path: services
task: run-one
next:
go: finished
- name: run-one
subworkflow:
definition_id: deploy-service
input_mapping:
_item: service_name
- name: finished
succeed: true
Terminal window
kis flow -f deploy.yaml -n deploy-all

This is how shared logic is reused: one flow becomes a subflow of another, with input_mapping: deciding what it receives. See Node types.

{{ }} is Liquid, so it does more than substitution:

tasks:
- name: report
shell: |
echo "Environment: {{environment | upcase}}"
echo "Services: {{services | size}}"
{% for svc in services %}
echo " - {{svc}}"
{% endfor %}

Templating is applied to parameter values before a node runs, so any atom’s parameters can carry it — not just shell:.

Two things to watch:

  • Quote a value that starts with {{. YAML reads a leading { as the start of a flow mapping, so shell: {{cmd}} is a parse error while shell: "{{cmd}}" is what you meant.
  • Parameter keys are matched case-sensitively, and flows spell them lowercase. workingdir: is the parameter; workingDir: is an unrecognised key and the atom quietly uses its default.