flux-platform 1.0.1

A local-first, AI-native developer automation platform: build, test, package, and deploy from a single .flux file, and make your repository legible to AI agents.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# The `.flux` configuration language

Flux has its own small declarative language. A `.flux` file is the single
source of truth for how a project builds, tests, packages, and ships.

## A complete example

```flux
# Comments start with '#' (or '//') and run to end of line.
project "my-app"
language rust

# Optional: run every command inside a container build environment.
environment { image "rust:latest" }

pipeline {
    step frontend { command "npm --prefix web run build" }
    step backend  { command "cargo build --release" }

    step tests {
        needs [ frontend, backend ]     # runs after both (which run in parallel)
        command "cargo test"
    }

    step security {
        needs tests
        tool scanner                    # hand off to an installed tool/plugin
    }

    step deploy {
        needs security
        command "./deploy.sh"
        only_if branch == "main"        # conditional
        retries 2                       # retry on failure
        timeout "5m"                    # kill the command if it hangs
        secrets [ DATABASE_URL ]        # inject an encrypted secret
    }
}

secret DATABASE_URL
deployment { target kubernetes replicas 3 }
```

## Top-level directives

| Directive                | Meaning                                             |
| ------------------------ | --------------------------------------------------- |
| `project "<name>"`       | The project name.                                   |
| `language <id>`          | `rust`, `node`, `python`, …                         |
| `environment { … }`      | A container build environment (see below).          |
| `secret <NAME>`          | Declares a secret the pipeline uses.                |
| `deployment { … }`       | A deploy target (see below).                        |
| `runners { … }`          | Declares runner pools (see *Runner pools*).         |
| `policy <name> { … }`    | An organization rule (see *Policies*).              |
| `pipeline { … }`         | The steps (and `use` of modules).                   |

All are optional. Omit `language` and Flux detects it; omit the `pipeline` and
Flux uses the language's default steps.

## Steps

`step <name> { <fields> }`. Every step needs **either** a `command` or a `tool`.

| Field                    | Meaning                                                       |
| ------------------------ | ------------------------------------------------------------- |
| `command "<shell>"`      | Shell command (`cmd /C` on Windows, `sh -c` elsewhere).       |
| `tool <id>`              | Hand off to an installed tool/plugin (e.g. `scanner`).        |
| `description "<text>"`   | Optional human description.                                   |
| `cache on` / `cache off` | Participate in the build cache (default `on`).                |
| `needs <a>` / `needs [ a, b ]` | Steps that must succeed first.                          |
| `secrets <A>` / `secrets [ A, B ]` | Declared secret names to decrypt and inject as environment variables. |
| `retries <n>`            | Retry the command up to *n* times on failure.                 |
| `timeout <n>` / `"10m"` / `off` | Kill one attempt of the command after this long (see *Timeouts*). |
| `only_if <var> == "v"`   | Run only when the condition holds (also `!=`).                |
| `inputs [ "glob", … ]`   | Scope the cache to these files (intelligent cache, 3.2).       |

`pipeline { … }` takes two fields of its own, which apply to the whole run:

| Field             | Meaning                                                        |
| ----------------- | -------------------------------------------------------------- |
| `timeout <dur>`   | Default limit for steps that declare none.                     |
| `parallel <n>`    | Cap on how many steps run at once.                             |

```flux
pipeline {
    timeout "10m"       # every step, unless it says otherwise
    parallel 4          # at most four commands running at a time

    step build { command "cargo build --release" timeout "30m" }
    step serve { command "./dev-server"          timeout off }
}
```

### Execution model

- With **no** `needs` anywhere, the pipeline runs as a **linear chain** in
  declared order (Phase 1 behaviour).
- With any `needs`, it becomes a **dependency graph**: steps without `needs` are
  roots and run in parallel; each step waits for its dependencies.
- If a step fails (after its retries), its transitive dependents are **skipped**.
- `only_if` that evaluates false marks the step **skipped**; this does *not*
  cascade — dependents still run.
- Each step's stdout and stderr are **streamed as they arrive**, every line
  tagged with the step it came from (see *Live output*).
- A step that outruns its **timeout** is killed and counts as a failure, so its
  dependents are skipped like any other failure.

## Live output

Steps do not run silently and they do not report in a lump at the end. Each line
is printed as the command produces it, prefixed with the step name and a glyph
for the stream it came from: `│` for stdout, `┆` for stderr.

```text
$ flux build
Pipeline:
  dependency graph · up to 4 steps in parallel
  plan: frontend → backend → tests
  step timeout: 30m
  queued frontend
  queued backend
  ▶ frontend  npm --prefix web run build
  ▶ backend  cargo build --release
  frontend │ vite v5.0.0 building for production...
  backend  │ Compiling serde v1.0.0
  backend  ┆ warning: unused import: `std::fmt`
  ✓ backend  (11.4s)
  frontend │ built in 3.21s
  ✓ frontend  (3.6s)
```

Two lines never interleave: a line reaches the terminal whole or not at all, so
four parallel compilers read as four labelled columns rather than shredded text.
Everything goes to stdout, including the steps' stderr, so `flux build > log`
captures the run in the order it happened. Colour is dropped when `NO_COLOR` is
set; the glyphs are not, because a redirected log is exactly where you need to
know which stream a line came from.

## Timeouts

Every attempt of every step runs under a wall-clock limit. The default is **30
minutes**, which no healthy build reaches and no hung command survives.

```flux
step deploy { command "./deploy.sh" timeout "5m" }   # quoted duration
step build  { command "cargo build" timeout 900 }     # bare number = seconds
step serve  { command "./dev-server" timeout off }    # no limit at all
```

- A quoted duration is a number and one unit: `s`, `m`, or `h`. Unquoted `10m`
  is a parse error that tells you to quote it, because reading it as ten seconds
  would be sixty times wrong and silent.
- A bare number is seconds. `timeout 0` is an error: write `timeout off`.
- The precedence is step field, then `pipeline { timeout … }`, then the 30-minute
  default. `timeout off` on a step beats a pipeline default, because "run this
  unbounded" is a decision rather than a missing value.

When the limit passes, Flux kills the command, prints
`✗ <step>  timed out after 5m (killed)`, suggests raising or removing the limit,
and fails the build. A timed-out attempt counts as a failed attempt, so `retries`
still applies: a step with `retries 2 timeout "5m"` can spend up to 15 minutes
before it gives up. That is the one place the two features multiply, and it is
worth reading twice before writing both.

Two honest limits on what a kill can reach:

- Flux kills the shell it started. A command that backgrounds children of its own
  (`./server &`) can leave them behind; Flux says the step timed out rather than
  claiming it reaped a process tree it cannot see.
- Under `environment { image … }` the command Flux kills is the container client.
  The container itself may keep running and need a `docker stop`.

## Parallelism

Independent steps run concurrently, capped by the worker pool. The default is the
machine's core count, clamped to 16.

```flux
pipeline {
    parallel 2          # these steps are memory-hungry; two at a time
    step a { command "make -j8 a" }
    step b { command "make -j8 b" }
}
```

`flux build --parallel 4` (or `-j 4`) overrides the file for one run, on
`build`, `test`, `run`, `ci`, and `workspace build`. The split is deliberate: how
many commands a machine can stand to run at once is a property of the machine and
belongs on the command line, while a step's timeout is a property of the step and
belongs in the committed `.flux`. `parallel 0` is a parse error; the minimum is 1,
which runs the graph in topological order one step at a time.

### Conditions

A condition is one variable, one operator, one string literal:

```text
only_if <var> ("==" | "!=") "<value>"
```

**The operators are `==` and `!=`, and that is the whole set.** There is no
`&&`, `||`, `<`, glob, or regex form. A step guard is a single equality test; a
pipeline that needs richer logic should branch inside its own command, where it
can be tested with the tools the project already has.

**The variables are these three, and nothing else parses:**

| Variable   | Value                                                                 |
| ---------- | --------------------------------------------------------------------- |
| `branch`   | Current git branch (`git branch --show-current`).                      |
| `tag`      | The tag on the current commit, if it is exactly one (`git describe --tags --exact-match`). |
| `flux_env` | The active secret environment: `FLUX_ENV`, or `default` when unset.    |

```flux
step deploy {
    only_if branch == "main"
}

step publish {
    only_if tag != ""          # only on a tagged commit
}

step migrate {
    only_if flux_env == "production"
}
```

All three are bound on every run. When Flux can't determine one (no git
repository, a detached HEAD, an untagged commit) it binds the empty string, so
`tag != ""` means "this commit is tagged" everywhere rather than "git answered
on this machine". `flux_env` is never empty.

A variable outside that table is a **parse error** naming the three that exist,
not a condition that silently compares against `""`. That is deliberate: a typo
in `only_if brunch == "main"` would otherwise skip your deploy step forever
without a word. It also means the namespace can be widened later without
breaking anything, since every name that is an error today is free to become
valid tomorrow. Nothing will be removed from it.

The colon style from the spec (`only_if:`, `secrets:`) is also accepted: a `:`
after a field keyword is ignored.

## Environments (containers)

```flux
environment { image "rust:latest" }
```

When set, each command runs inside that image via Docker or Podman
(`docker run --rm -v <project>:/workspace -w /workspace <image> sh -c '<cmd>'`).
If no engine is installed, Flux runs the command natively and says so.

## Deployment

```flux
deployment {
    target kubernetes     # local | docker | kubernetes | vm
    replicas 3
    image "myapp:1.0"     # optional
}
```

`flux deploy` dispatches to the target. For `kubernetes` it generates a real
Deployment manifest under `.flux-cache/deploy/` and applies it with `kubectl`
when available.

## Secrets

Declare with `secret NAME`, set with `flux secret set NAME value` (encrypted at
rest), and inject with a step's `secrets [ NAME ]`. Set per-environment values
with `--env` (e.g. `flux secret set DB_URL … --env production`); the pipeline
reads the environment named by `FLUX_ENV` (default `default`), which is also
what the `flux_env` condition variable tests.

A secret named in a step but never set is injected as the empty string, and the
step says so as it runs.

## Intelligent cache (3.2)

By default a step's cache tracks the whole project. Declare `inputs` to scope it:

```flux
step frontend { command "npm run build" inputs [ "frontend/**" ] }
step backend  { command "cargo build"   inputs [ "backend/**" ] }
```

Now editing a `backend/` file leaves `frontend` cached. Because the engine knows
the graph, any step that `needs` a *rebuilt* step is itself rebuilt — so only
the affected packages and their downstream steps run. Globs support `**` (any
depth), `*` and `?` (within a path segment).

## Modules (3.3)

Put a reusable pipeline in `modules/<name>.flux` and pull it in with `use`:

```flux
# modules/rust-library.flux
pipeline {
    step deps  { command "cargo fetch" }
    step build { command "cargo build --release" }
    step test  { command "cargo test" }
}
```

```flux
# .flux
project "my-api"
language rust
pipeline {
    use rust-library        # splices in deps/build/test
    step package { command "docker build ." }
}
```

Module steps are spliced ahead of the pipeline's own; explicit steps win on
name collisions. `use` is the only way to pull in a module.

## Runner pools (3.1)

```flux
runners {
    pool "gpu-builders" {
        requirements { gpu true, memory "32gb" }
    }
    pool "linux" { os linux }
}
```

View pools and registered runners with `flux runners list`. Pools are a
declaration of the machines a project expects to build on; they are *not* wired
to scheduling, because scheduling across machines is part of the deferred
distributed runner network and locally the graph engine only has this machine.

A step used to be able to name a pool with `pool "gpu-builders"`. That field
never reached a scheduler, so it was removed rather than left implying the step
would run somewhere in particular; the `runners` block stays.

## Policies (4.15)

Declare organization rules a pipeline must satisfy before it ships:

```flux
policy production {
    require tests
    require security
    require approvals 2
}
```

`flux policy` checks the current pipeline; `flux ci` refuses to run when a policy
is violated. `require tests` needs a step whose name contains `test`; `require
security` needs a step named `security` or any `tool` hook; `require approvals N` is
satisfied by the `FLUX_APPROVALS` environment variable (Flux has no identity
system of its own).

## Workspaces (4.1/4.2)

A `flux.workspace` file (separate from `.flux`) manages multiple projects:

```text
workspace "backend"

member shared  { path "shared" }
member auth    { path "services/auth"    needs [ shared ] }
member gateway { path "services/gateway" needs [ auth, shared ] }
```

`flux workspace build` builds members in dependency order and rebuilds only those
affected by changes (a member whose files changed, plus everything downstream) —
the intelligent cache extended across repositories. `flux workspace status` shows
which members are affected.

## Templates (4.6)

`flux init <template>` writes a curated `.flux` instead of a bare default:
`rust-api`, `react`, `node-service`, `library`, `cli`.

## Grammar

```text
config    := item*
item      := "project" STRING
           | "language" IDENT
           | "environment" "{" ("image" STRING)* "}"
           | "secret" IDENT
           | "deployment" "{" dep_field* "}"
           | "runners" "{" pool* "}"
           | "policy" name "{" require* "}"
           | "pipeline" "{" (step | use)* "}"
dep_field := "target" IDENT | "replicas" NUM | "image" STRING
pool      := "pool" name "{" pool_field* "}"
pool_field := requirement_field
           | "requirements" "{" requirement_field* "}"
requirement_field := "os" name | "gpu" bool | "memory" name
require   := "require" ("tests" | "security" | "approvals" NUM)
use       := "use" name
step      := "step" IDENT "{" field* "}"
field     := "command" STRING | "tool" IDENT | "description" STRING
           | "cache" IDENT | "needs" ident_or_list | "secrets" ident_or_list
           | "inputs" ident_or_list
           | "retries" NUM | "only_if" cond_var ("==" | "!=") STRING
ident_or_list := item_or_str | "[" (item_or_str ("," item_or_str)*)? "]"
item_or_str   := IDENT | STRING
name          := IDENT | STRING
cond_var      := "branch" | "tag" | "flux_env"
bool          := "true" | "yes" | "on" | anything else (false)
```

Inside `[ … ]` lists and `requirements`/`policy`/`pool` blocks, commas are
optional separators — they are skipped wherever they appear.

Strings support `\n`, `\t`, `\"`, `\\`. Identifiers are
`[A-Za-z_][A-Za-z0-9_.-]*`. Parse errors report a 1-based line number.

## Retired keywords

Three keywords parsed but did nothing. They were removed before 1.0 rather than
carried into a stability promise they would have made a lie.

| Keyword                   | Status                        | Replacement                                   |
| ------------------------- | ----------------------------- | --------------------------------------------- |
| step `env [ … ]`          | Deprecated, removed next release | `secrets [ … ]`, same meaning under an honest name |
| step `pool "<name>"`      | Removed (parse error)         | Declare pools in `runners { … }`; there is no per-step preference |
| top-level `import <name>` | Removed (parse error)         | `use <name>` inside `pipeline { … }`          |

`env` never meant environment variables. It always took *declared secret names*.
It still parses, still sets `secrets`, and prints a deprecation warning naming
the file and line; `flux format` rewrites it. After it is removed, `env` is free
to mean actual environment variables.

`pool` and `import` are hard errors that name their replacement, because a
silent no-op is precisely what made them worth removing.