Skip to content
Talk to our solutions team

Authoring Flows

A complete guide to writing flows (workflows / pipelines) for the AI Flow runtime.

Who this is for. Anyone authoring a flow definition — whether you run it locally with the kis flow CLI or deploy it to aiflow.svc as a tenant workflow. No Go knowledge required.

A flow is a directed graph of tasks. Each task does one thing — run a shell command, call an HTTP endpoint, make a decision, iterate over a list, wait for a signal. Tasks are connected by routing (next: for success, error: for failure), and they share a single bag of data called the context.

The runtime detects the kind of each node from which key you use — you never write a nodetype: field. shell: makes it a shell task; choice: makes it a decision; map: makes it a parallel fan-out. That is the single most important idea in this guide.

Flows are executed by the flow engine. When run under aiflow.svc, each Task node is dispatched to a remote agent chosen by the agent fabric based on tags, model, GPU/CPU, and capacity.

name: hello-world
tasks:
- name: greet
print: "Hello, World!"

Run it:

Terminal window
kis flow -f hello-world.yaml

A slightly richer one that uses variables, a shell task, and routing:

name: build-and-greet
vars:
who: World
tasks:
- name: build
shell: |
echo "building..."
next:
go: greet
- name: greet
print: "Done, {{who}}!"

Override a variable at run time with -v:

Terminal window
kis flow -f build-and-greet.yaml -v who=Team

Three shapes are accepted. All three parse to the same thing.

Workflow keys sit at the document root:

name: my-flow
version: "1.0.0"
tasks:
- name: step-one
print: "hi"

Self-describing; everything lives under flow::

flow:
name: single-flow-example
version: "1.0.0"
vars:
greeting: Hello
tasks:
- name: show
print: "{{greeting}}"

Several definitions in one file. Select which to run with -n <name>. Also the idiomatic way to keep a parent flow next to the child it calls via SubWorkflow:

flows:
- name: build
tasks:
- name: compile
print: "building"
- name: deploy
vars: { env: staging }
tasks:
- name: ship
print: "deploying to {{env}}"
Terminal window
kis flow -f flows.yaml # lists the flows
kis flow -f flows.yaml -n deploy # runs the "deploy" flow
KeyTypeMeaning
idstringDefinition ID (the store key). Auto-generated (ULID) for script flows and inline HTTP defs; set it explicitly for stable references.
namestringHuman name; usable as a lookup handle.
descriptionstringFree text.
versionstringDefaults to "1.0.0".
typestringWhich task registry / registry type to use.
varsmapWorkflow variables → seed the initial context. Overridable with -v key=value.
starttaskstringFirst node to run (defaults to the first task in the list).
activeboolDefault true.
loglevelstringdebug / info / …
userstringOS user for shell tasks.
workingdirectorystringCWD for shell tasks (~ expands to home).
continueonerrorboolDefault true for DAG, false for array.
importsmapAlias map: bare name → URI, substituted into file/src/path/ref fields at parse time.
tablesmapNamed datasets for data-driven tasks (see §15).
affinitymapAgent affinity policy (see §11).
accessmap/stringWorkflow-level authorization (see §14).
pipeline / list / statesExecution-mode selectors (see below).

Execution mode is chosen by these keys:

You writeYou get
(nothing special — routing via next:/error:)DAG (default)
list: trueArray (sequential)
states: [...]Stateflow (state machine)
pipeline: trueDataPipeline (source/sink streaming)

Every entry under tasks: is one node. The keys you can use:

- name: deploy # REQUIRED, unique within the flow
type: shell # optional explicit task type…
shell: "make deploy" # …or use the task type AS the key (shorthand)
vars: { region: us-west-2 } # task-scoped variables
check: "env === 'prod'" # precondition — skip the node if false
selectors: { gpu: true } # which agent should run it
group: build # co-locate with other tasks in this group
retry: { max_attempts: 3 } # retry policy
timeout: 5m # execution timeout
final: false # force-run even after errors; mark terminal
input: [raw] # named data-pipe input ports (pipeline mode)
output: [clean] # named data-pipe output ports (pipeline mode)
before: [ ... ] # pre-hooks
after: [ ... ] # post-hooks
access: "user.role == 'admin'"
next: { go: verify } # success routing
error: { go: rollback } # failure routing

If a task has no type:, the first non-reserved key becomes both the task type and its body. These two are identical:

- name: build
type: shell
shell: "go build ./..."
- name: build
shell: "go build ./..."

shell: → task type shell, execute body "go build ./...". Likewise print:, httpclient:, git:, and any other registered task type (see §19).

Edges are next: (on success) and error: (on failure). Each is an “on” block:

next:
go: default-target # unconditional target node (REQUIRED)
set: { flag: true } # inject vars into context before routing
when: # optional ordered conditions — first match wins
- code: "status === 'ok'"
language: js # optional, default "js"
go: ok-target
set: { verified: true }
- code: "status === 'retry'"
go: retry-target

Rules:

  • go: is required — on the block and on every when: entry.
  • when: conditions are evaluated in order; the first match routes to its go: and applies its set:. If none match, the block’s top-level go: is used.
  • A Task node with neither next: nor error: is automatically final (terminal).
  • set: values are injected into the context at the moment of the transition.

Choice nodes route differently — they use rules[].next / default: inside the choice: block, not the node’s next:/error:. See §7.3.

A unit of work dispatched to a worker/agent.

- name: run-tests
shell: |
go test ./...
next: { go: report }
error: { go: notify-failure }

Anything that isn’t one of the control keys below is a Task. The task type must resolve to a registered task (§19).

Inject or reshape variables in the context. Runs inline (no worker). pass: is a backward-compat alias.

- name: set-defaults
assign:
set:
region: us-west-2
environment: staging
features:
dark_mode: true
next: { go: next-step }

Use it for defaults, computed values, or reshaping data between tasks.

Conditional routing. Rules are JavaScript expressions evaluated against the context; first match wins; falls through to default:.

- name: route-by-status
choice:
rules:
- condition: "status === 'approved' && priority === 'high'"
next: fast-track
- condition: "status === 'approved'"
next: standard-approval
- condition: "status === 'rejected'"
next: handle-rejection
default: needs-review

Choice uses rules[].next and default:not the node’s own next:/error:. If no rule matches and there is no default:, the workflow errors.

Sequential iteration — one item at a time.

- name: health-check-each
foreach:
items_path: servers # dot-path to a list already in context
task: check-server # the task to run per item
next: { go: report }
- name: check-server
shell: 'echo "checking {{_index}}: {{_item.host}}:{{_item.port}}"'

Per iteration you get _item, _index, _foreach_node. When done, _results holds the collected results. An empty collection skips straight to next:.

items_path: reads a list already in context. To iterate a computed/declarative source, use iterator: instead (see §9). You can also target sub_workflow_id: instead of task:.

Parallel fan-out — all items at once, throttled by max_concurrency.

- name: build-all
map:
items_path: images
task: build-image
max_concurrency: 3 # 0 = unlimited
next: { go: summary }
- name: build-image
shell: 'echo "[{{_index}}] building {{_item.name}}:{{_item.tag}}"'

Same injected variables as ForEach, but _map_node instead of _foreach_node. Actual parallelism = min(items, max_concurrency, available workers) — so pass enough workers (-w N) locally to see it. Each branch gets an isolated context clone, so parallel branches never race. _results is indexed by position (and nests for map-in-map).

Suspend the instance until a signal arrives or a timeout fires. This is how you build human-in-the-loop approvals and event-driven flows.

- name: wait-for-approval
wait:
timeout: 1h
events:
- name: approved
required: [approver_name] # signal must carry these fields
go: deploy # static routing on this event
- name: rejected
required: [reason]
go: rollback
error: { go: handle-timeout } # timeout follows the error route
  • required: validates the signal payload before the event is accepted.
  • go: is static routing. For dynamic routing, use on: with a handler that calls flow.go("node"):
- name: wait-for-score
wait:
timeout: 30m
events:
- name: review_completed
required: [score, reviewer]
on: |
if (signal.score >= 80) flow.go("fast-track");
else if (signal.score >= 50) flow.go("manual-review");
else flow.go("reject");

Signal data lands in _signal (full payload) and is also flattened to top-level context.

Send a signal (server mode):

Terminal window
kis signal <instanceID> approved -d approver_name=alice
POST /workflow/signal {"instanceid":"...", "signal_name":"approved", "data":{"approver_name":"alice"}}

Delegate to a child flow. The parent suspends while the child runs, then resumes with mapped output. flow: is the preferred key; subworkflow: is an alias.

flows:
- name: child-deploy
tasks:
- name: do-deploy
shell: 'echo "deploying {{service_name}} to {{target_env}}"'
next: { go: done }
- name: done
assign: { set: { deploy_status: success } }
next: { go: finish }
- name: finish
succeed: true
- name: parent-pipeline
tasks:
- name: run-deploy
subworkflow:
definition_id: child-deploy # or `ref: child-deploy`
input_mapping: # parent → child
app_name: service_name
environment: target_env
output_mapping: # child → parent
deploy_status: result_status
next: { go: summary }
- name: summary
print: "status={{result_status}}"

Empty mappings pass/merge the full context. Preferred keys: flow:/ref:/inputs:/outputs:; legacy aliases: subworkflow:/definition_id:/input_mapping:/output_mapping:.

Terminate the instance with a typed error. Inline; automatically final.

- name: invalid-config
fail:
error: ConfigError
cause: "Unknown strategy. Use rolling, blue-green, or canary."

Short form: fail: "message"{error: UnspecifiedError, cause: "message"}. Sets _error and _cause in context.

Terminate the instance successfully. Inline; automatically final. If the instance is a nested child, this resumes its parent.

- name: done
succeed: true

There is one shared bag of data per run: the context (smap.Map). It starts from your vars: plus -v overrides (which win), and every mechanism reads and writes it.

String values (shell commands, print messages, iterator configs) are rendered with Liquid:

print: "Deploying {{app}} v{{version}} to {{region}}"
print: "processed {{ _results | size }} items" # filters work
shell: 'echo "{{_item.host}}"' # dot-paths work

Rendering is single-pass: when an Assign sets msg: "{{greeting}}", it stores the expanded value at that point — not a live reference that keeps tracking greeting.

Conditions (the platform scripting runtime)

Section titled “Conditions (the platform scripting runtime)”

Routing/decision expressions (choice rules, when:, check:, wait on:) are not Liquid — they are evaluated by the platform scripting runtime (default JavaScript). Context keys are available as top-level globals:

choice:
rules:
- condition: "env === 'production' && approved === true"
next: deploy-prod
VariableSet byMeaning
_itemForEach, Mapcurrent iteration item
_indexForEach, Mapcurrent index (0-based)
_foreach_node / _map_nodeForEach / Mapthe iterating node’s name
_resultsForEach/Map on completioncollected results (indexed; nested for map-in-map)
_resultany task completionthe last task’s result
_error / _causeFailerror type / cause
_signalWait (signal)the full signal payload
_wait_timeout / _wait_nodeWait (timeout)timeout flag + node name

Inside a Map branch, _item refers to that branch’s item. To keep an outer value visible after an inner map shadows _item, stash it with an Assign (region_name: "{{ _item }}") — see the map-in-map recipe in §20.

ForEach and Map can source items from a declarative iterator instead of a context list. Use iterator: in place of items_path:. The type: selects the source; each produced row becomes one _item.

# numbers
- name: sweep
foreach:
iterator: { type: numbers, from: 1, to: 5, step: 1 }
task: train-shard
# inline array
- name: per-model
map:
iterator: { type: array, items: ["gpt", "claude", "llama"] }
task: bench
max_concurrency: 2
# scalar items are boxed as {value} → reference {{ _item.value }}
# files on disk
- name: ingest-docs
map:
iterator: { type: files, path: "{{repo_path}}", glob: "*.yaml", recursive: true }
task: ingest
max_concurrency: 4

Available source types include: numbers, array, time-window, lines, files, dirs, git-repos, shell-output (and they nest — map-in-map). Iterator config strings are Liquid-rendered against the context first, so path: "{{repo_path}}" works.

Capability gating. Filesystem/exec/network iterators require capabilities granted to the engine. The local kis flow CLI grants all; a tenant flow running inside a service leaves them denied unless configured. Prefer items_path: + a prior task for portable flows.

items_path: vs iterator: — use items_path: to iterate a list already in context (produced upstream); use iterator: for declarative/computed sources (a number range, files on disk, lines of a file).

Selectors — route a task to the right agent

Section titled “Selectors — route a task to the right agent”
- name: gpu-training
shell: "train.py"
selectors:
gpu: true # require a GPU
freegpu: 50 # …with ≥50% free VRAM
- name: cpu-job
shell: "process.sh"
selectors:
cpu: 40 # ≥40% free CPU
memory: 30 # ≥30% free memory
- name: inference
shell: "infer.sh"
selectors:
tag: inference # only agents tagged "inference"
- name: on-gpt4
shell: "run.sh"
selectors:
model: gpt-4 # only agents with the gpt-4 model loaded

Supported keys: tag, model, gpu, freegpu, cpu, memory, id/workerid, group. In single-worker (CLI) mode selectors are a no-op — there is only one agent.

You can also pass selectors per-task at start time over HTTP:

{
"workflowname": "selector-example",
"selectors": {
"gpu-training": { "gpu": true, "freegpu": 50 },
"inference": { "tag": "inference" }
}
}

Task groups — co-locate tasks on one agent

Section titled “Task groups — co-locate tasks on one agent”

Tasks in the same group: are guaranteed to run on the same agent — useful when they share local state (build artifacts, a workspace, model weights).

- name: build
shell: "make build" # writes /tmp/build
group: compile
next: { go: test }
- name: test
shell: "make test" # reads /tmp/build — same machine
group: compile

Groups are lighter than affinity: they scope co-location to a set of tasks within one run.

Affinity pins execution to specific agents across runs. Configure it at the workflow level:

name: my-workflow
affinity:
scope: single-agent # none | single-agent (instance) | definition | payload
payload_fields: [env, region] # only for payload scope
restart: false # false = resume on agent failure; true = restart
tasks:
- name: deploy
shell: "deploy.sh"
ScopeEffect
noneNo pinning.
single-agent / instanceAll tasks of one run go to one agent.
definitionAll runs of this definition share the same agent.
payloadPin by the values of payload_fields.

At start time you can override with params: pin-agent, affinity, affinity-key, restart-on-failure. On agent failure the binding is cleared and the run resumes (reassign) or restarts per restart.

- name: unreliable-task
shell: |
curl -f https://flaky.example.com
retry:
max_attempts: 3
backoff_base: 500ms
backoff_max: 5s
backoff_multiplier: 2.0 # default 2.0
jitter_fraction: 0.1 # default 0.1
retryable_errors: [Timeout, ConnReset] # optional
non_retryable_errors: [AuthError] # optional
next: { go: success }
error: { go: all-retries-failed } # taken after retries are exhausted

Delay = min(backoff_base · multiplier^attempt, backoff_max) + jitter. When attempts are exhausted, routing follows error:.

- name: slow-task
shell: "long-running.sh"
timeout: 5m # execute-phase timeout

Fine-grained form (NodeTimeouts): total, before_execute, execute, after_execute. There is currently no instance-level timeout — bound long-running flows per node.

Two different mechanisms — don’t confuse them:

  • check: is a precondition on a task. If it evaluates false, the node is skipped.

    - name: deploy-prod
    check: "env === 'production'" # bare string → {expression, language: js}
    shell: "deploy-prod.sh"
  • when: lives inside a next:/error: block and chooses which edge to follow (see §6).

Both use the platform scripting runtime (default JS). A bare string is shorthand for { expression, language: js }.

access: gates execution by caller identity, evaluated via the platform scripting runtime. It composes at two scopes:

# workflow-level — gates the whole flow at init
access:
rule: "user.role == 'admin' || user.role == 'operator'"
language: js
ondeny: fail # fail (default, loud) | skip | sentinel | drop
tasks:
- name: sensitive-op
access: "user.role == 'admin'" # per-task; ANDed with workflow-level
shell: "rotate-keys.sh"

AccessConfig fields: rule, language, ondeny, replace (a task with replace: true overrides the workflow rule instead of ANDing). Deny actions: fail, skip, sentinel, and drop (Map/ForEach only — filters the item out). This is distinct from check: (a data precondition, not an authorization gate).

Define named datasets and run a task once per row, with columns available as {{col}}:

list: true
continueonerror: true
tables:
services: { type: csv, file: ./services.csv } # also: excel (sheet:), or inline pipe-delimited
tasks:
- name: build
table: services
check: '"{{build}}" === "TRUE"'
shell: |
cd {{folder}}/{{projectname}}
go build ./...

Table types: csv (with separator), excel (with sheet), or an inline pipe-delimited string. Files resolve through the repo resolver, falling back to the local filesystem.

Set pipeline: true to build a pipe-and-filter data flow. Tasks declare named input:/output: ports; the planner wires a producer’s output to the matching consumer’s input, and records stream between workers through the data pipe.

name: etl-pipeline
pipeline: true
tasks:
- name: extract
type: httpclient
output: [raw]
- name: transform
type: jq
input: [raw]
output: [clean]
- name: load
type: db
input: [clean]

Records are fed in and inspected via the engine’s SendDataPipeRecord / GetActivePipelines (this is exactly what aiflow.svc’s POST /workflow/request does under the hood for conversational pipelines). Optional per-task the platform data layer validation (ValidateDatastore / ValidateEntityName) can validate records against an entity schema.

17. Scripting flows in JS / Lua / Starlark

Section titled “17. Scripting flows in JS / Lua / Starlark”

When YAML isn’t expressive enough, author the flow as a script (.js, .lua, .star). Two modes:

Return an object with the same shape as YAML. The last expression is consumed:

var workflow_def = {
name: "js-export-example",
version: "1.0.0",
vars: { env: "staging", app_name: "my-service" },
tasks: [
{ name: "setup", assign: { set: { region: "us-west-2" } }, next: { go: "route" } },
{ name: "route", choice: { rules: [
{ condition: "env === 'production'", next: "deploy-prod" }
], default: "deploy-dev" } },
{ name: "deploy-prod", print: "PROD deploy", next: { go: "done" } },
{ name: "deploy-dev", print: "DEV deploy", next: { go: "done" } },
{ name: "done", succeed: true },
],
};
workflow_def; // ← returned to the runtime

Mode B — imperative (the flow namespace)

Section titled “Mode B — imperative (the flow namespace)”

Each flow.* call appends a task:

flow.name("imperative-deploy");
flow.vars({ strategy: "rolling", version: "3.0.0" });
flow.pass("init", { deploy_version: "{{version}}" }, { next: "choose" });
flow.choice("choose", {
rules: [
{ condition: "strategy === 'rolling'", next: "rolling" },
{ condition: "strategy === 'blue-green'", next: "bluegreen" },
],
default: "unknown",
});
flow.print("rolling", "ROLLING deploy", { next: "done" });
flow.print("bluegreen","BLUE-GREEN deploy", { next: "done" });
flow.fail("unknown", "ConfigError", "Unknown strategy");
flow.succeed("done");

The same in Lua:

flow.name("lua-workflow-example")
flow.vars({ greeting = "Hello", target = "World" })
flow.pass("setup", { timestamp = "2026-02-12" }, { next = "show" })
flow.print("show", "{{greeting}}, {{target}}!", { next = "done" })
flow.succeed("done")

Full namespace: name, description, version, loglevel, vars, task, shell, print, script, assign (pass alias), choice, foreach, map, wait, fail, succeed, subworkflow. A trailing opts object’s next/error accept a string (→ {go: str}) or an object.

Terminal window
kis flow -f my-flow.yaml # run a bare/single flow
kis flow -f flows.yaml -n deploy # pick one flow from a multi-flow file
kis flow -f my-flow.yaml -v env=prod # override a variable
kis flow -f my-flow.yaml -w 4 # provide 4 workers (see Map parallelism)
kis signal <instanceID> approved -d approver_name=alice # send a signal to a Wait node

The CLI binary may be aliased per environment (kis, kis-dev, kis-morph, …); the flags are the same.

Deploy the flow as a tenant workflow (delivered through product config), then:

Terminal window
# async start
curl -X POST https://<host>/workflow/start \
-H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"workflowname":"my-flow","context":{"env":"prod"}}'
# synchronous start (blocks for responses)
curl -X POST https://<host>/workflow/start/sync/response \
-H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"workflowname":"my-flow","context":{"env":"prod"}}'
# resume / signal / status
curl -X POST https://<host>/workflow/signal -d '{"instanceid":"...","signal_name":"approved","data":{}}'
curl "https://<host>/workflow/status?instanceid=..."

See the [HTTP API](/api/AI Flow/) for the full surface.

Task types come from the registry the service is built with (aiflow.svc uses the AI-Flow registry, which includes the common + devops task sets). Commonly available types:

Shell & scripting: shell, supershell, script, python, mojo, ssh Files & data: file, glob, archive, extract, template/liquid, yaml, json, jq Network & transfer: httpclient/http, scp, rsync, git, s3, azureblob, scraper Data stores: db Secrets & crypto: vault, secret, encrypt/decrypt, hash, acme/legodns, credential-provider Utility: print, setenv, setvars, id, rule, suspend, throw DevOps (vulcan set): cron, port, coredns, user, deployproduct, infra, kong, process, scaffold, service

The exact set depends on the deployed registry. GET /workflow/config reports what a tenant has available. If a task type isn’t registered, planning fails with a clear error.

- name: build-all
map: { items_path: services, task: build-one, max_concurrency: 4 }
next: { go: summary }
- name: build-one
shell: 'echo "building {{_item.name}}"'
- name: summary
print: "built {{ _results | size }} services"
final: true
- name: request-approval
print: "awaiting approval…"
next: { go: gate }
- name: gate
wait:
timeout: 24h
events:
- name: approved
required: [approver]
go: deploy
- name: rejected
required: [reason]
go: abort
error: { go: escalate } # timeout
- name: setup
assign: { set: { regions: [us, eu, ap], services: [api, web] } }
next: { go: each-region }
- name: each-region
map: { items_path: regions, task: region-branch, max_concurrency: 3 }
next: { go: done }
- name: region-branch
assign: { set: { region_name: "{{ _item }}" } } # stash before inner _item shadows it
next: { go: each-service }
- name: each-service
map: { items_path: services, task: deploy-one, max_concurrency: 2 }
- name: deploy-one
shell: 'echo "[{{ region_name }} / {{ _item }}] deploying"'
- name: done
print: "done across {{ _results | size }} regions"
final: true

_results comes back nested: [[api,web], [api,web], …] per region.

- name: call-api
httpclient: { url: "https://api.example.com" }
retry: { max_attempts: 3, backoff_base: 1s, backoff_max: 30s }
next: { go: process }
error: { go: use-cache }

See §22 for a full worked example combining Assign → Choice → Map → ForEach → Succeed/Fail.

  • Node type comes from the key. shell:, choice:, map: etc. are the type — there’s no nodetype: field. A typo in the key silently makes it a plain Task.
  • Choice routes with rules[].next, not the node’s next:. Adding a next: to a Choice node does nothing useful.
  • go: is mandatory in every next:/error:/when:. Missing it is a parse error.
  • Templating is single-pass Liquid; conditions are the platform scripting runtime JS. Don’t put {{ }} in a condition: or check: — reference the bare variable (env === 'prod').
  • Map needs workers. With one worker (default CLI), a Map runs effectively sequentially. Pass -w N, or run against a cluster.
  • _item shadows in nested maps. Stash outer values with an Assign before an inner Map.
  • Prefer items_path: over filesystem iterators for flows that must run inside a tenant service (iterator capabilities are denied there by default).
  • Give nodes stable, unique names — they’re referenced by go: and used for resume/signal. Duplicate names are a parse error.
  • Bound long tasks with timeout: — there’s no instance-level timeout.
  • Set an explicit id: for flows you deploy and reference by ID; script/inline flows get a random ULID otherwise.
  • A Task with no routing is terminal. If a middle node “ends” the flow unexpectedly, it’s probably missing a next:.

A deployment pipeline exercising most node types:

name: deployment-pipeline
loglevel: info
vars:
strategy: blue-green
version: 2.1.0
tasks:
# 1. Setup — inject config
- name: init-config
assign:
set:
services:
- { name: api, port: 8080, replicas: 3 }
- { name: worker, port: 9090, replicas: 2 }
- { name: gateway, port: 443, replicas: 2 }
targets:
- { host: prod-01.example.com, zone: us-east }
- { host: prod-02.example.com, zone: us-west }
deploy_version: "{{version}}"
next: { go: choose-strategy }
# 2. Decide — route by strategy
- name: choose-strategy
choice:
rules:
- condition: "strategy === 'rolling'"
next: build-services
- condition: "strategy === 'blue-green'"
next: build-services
- condition: "strategy === 'canary'"
next: setup-canary
default: invalid-strategy
- name: invalid-strategy
fail: { error: ConfigError, cause: "Unknown strategy. Use rolling, blue-green, or canary." }
- name: setup-canary
print: "Setting up canary (10% traffic split)…"
next: { go: build-services }
# 3. Build all services in parallel
- name: build-services
map: { items_path: services, task: build-one-service, max_concurrency: 4 }
next: { go: deploy-to-targets }
- name: build-one-service
shell: |
echo "Building {{_item.name}}:{{deploy_version}} ({{_item.replicas}} replicas)…"
# 4. Deploy to each target sequentially
- name: deploy-to-targets
foreach: { items_path: targets, task: deploy-to-server }
next: { go: verify }
- name: deploy-to-server
selectors: { tag: deployer }
retry: { max_attempts: 2, backoff_base: 2s, backoff_max: 20s }
shell: |
echo "Deploying to {{_item.host}} (zone {{_item.zone}}), strategy {{strategy}}"
error: { go: deploy-failed }
- name: deploy-failed
fail: { error: DeployError, cause: "A target failed to deploy." }
# 5. Verify and finish
- name: verify
print: "Running post-deployment health checks…"
next: { go: deployment-complete }
- name: deployment-complete
succeed: true
  • [Concepts](/blocks/ai/AI Flow/concepts/) — the mental model behind flows, instances, agents.
  • [HTTP API](/api/AI Flow/) — starting/resuming/signaling flows over HTTP.
  • [Orchestration](/blocks/ai/AI Flow/orchestration/) — how agents are selected and how selectors resolve.
  • the flow engine reference docs — the code-accurate reference the parser follows.
  • the flow engine ships runnable example flows — 27 of them, covering v2 nodes and iterators.