A workflow engine for pipelines of AI agents. Two words carry the whole model, and they are not interchangeable:
- An assembly line is a definition: a graph of nodes and the edges between them, stored and versioned by its own content. It describes work; it never does any.
- An assembly run is one execution of one assembly line. Walking it opens a visit at each node it reaches, and a visit is worked by an agent in a pod, by a service worker, or by a person.
One assembly line has many assembly runs. Floor stores the definitions, decides what happens next, and hands the work out — it does none of the work itself.
Vocabulary, from lore's ADR-024: Factory ⊃ Floor ⊃ AssemblyLine ⊃ Station ⊃ Agent.
- Floor holds no provider client. No Kubernetes, no GitHub, no chat. Anything provider-shaped is a station or a separate app.
- Floor never calls out. Every worker pulls its own work from the queue. One exception, and it is deliberate: a pod's git credential, because git waits thirty seconds for an answer.
- No code of ours runs inside an agent pod. The ai-agent-subsystem's supervisor does that work.
A fourth, about state: visits are the truth, events are how they change. An assembly run's status, its bag, its current node and its cost are all derived by replaying its visits. None of them is stored.
flowchart TB
subgraph inside["one process, linked in memory"]
api["<b>apps/api</b><br/>HTTP routes, and the loop:<br/>route, dispatch, sweep"]
store["<b>packages/store</b><br/>seven tables, the events queue"]
kernel["<b>packages/assembly-lines</b><br/>the walk kernel, pure"]
api --> store --> kernel
end
pg[("Postgres")]
api --- pg
cluster["<b>apps/cluster-agent</b><br/>one per cluster"]
worker["<b>a service worker</b><br/>written with packages/station"]
lore["<b>lore</b><br/>owns GitHub"]
tools["<b>packages/pipeline</b><br/><b>packages/lore-converter</b>"]
client(["<b>packages/client</b><br/>typed HTTP and the live socket"])
k8s[["Kubernetes,<br/>the agent pods"]]
cluster --> client
worker --> client
lore --> client
tools --> client
client -. HTTP .-> api
cluster --> k8s
A solid arrow is a dependency linked in memory; a dashed one is HTTP. Nothing points out of the api: every worker calls in.
That split is the point. Only apps/api links the store. A worker cannot reach the database
even by accident, so the queue is the only way in, and a worker can live in another repository,
another language or another cluster without floor knowing.
sequenceDiagram
participant Outside as lore, a schedule,<br/>a person
participant Api as apps/api
participant Queue as the events queue
participant Worker as a worker
participant Pod as an agent pod
Outside->>Api: POST /events
Api->>Api: route it: start a run, or join<br/>the one open on this subject
Api->>Api: replay the visits, pick the next node
Api->>Api: open a visit, freeze its brief
Api->>Queue: enqueue a dispatch, tagged
Worker->>Queue: claim by offering tags
alt an agent station
Worker->>Pod: create one Agent resource
Pod->>Api: GET the brief, then post turns,<br/>costs and files to /sink
Pod->>Api: the lifecycle event that ends the visit
else a service station
Worker->>Worker: run one function
Worker->>Api: post a report
end
Api->>Api: write the report and advance the<br/>walk, in one transaction
Note over Api: the next node's start event,<br/>or the run settles
- Something posts an event to
POST /events— a webhook feeder, a schedule tick, a person. - The loop routes it. One api replica holds a Postgres advisory lease and runs the loop; every replica still serves HTTP. The event starts an assembly run, or joins the run already open on its subject.
- The walk picks the next node by replaying the run's visits — pure, in
assembly-lines. The assembly line says what may happen; the run's own visits decide what happens next. - A visit opens, its brief frozen then and there: each need resolved from the run's bag, the station and agent definition pinned by content hash, a deadline set.
- A dispatch goes on the queue, tagged. Nothing is pushed; workers claim by offering tags.
kind:agent→apps/cluster-agentclaims it and creates one Agent resource. The subsystem runs the pod; the pod posts what it does back to/station-runs/:id/sink.station:<name>→ a service worker claims it, works, posts a report.- A human node dispatches nothing: it waits with no deadline. Floor serves no page for it — whoever maintains the station builds that, and answers with a report or an event.
- A node with no station is a marker: it opens and reports success in one transaction.
- A report lands, and in the same transaction the walk advances: the next node's start event, or the run settles.
The bag is the only channel between nodes, and it is write-forward only. A node declares what
it needs and what it produces; floor moves the bytes and nothing else crosses.
| piece | what it is |
|---|---|
apps/api |
The HTTP API and the single-leased loop. The only process that touches Postgres. |
apps/cluster-agent |
The only process that talks to one cluster's Kubernetes API. Turns a dispatch into an agent pod. |
packages/store |
Six tables, the definitions store, the events queue, and the run store. |
packages/assembly-lines |
The walk kernel: which edge is taken next, given the visits so far. Pure, no I/O. |
packages/contracts |
The floor's wire, as types. No runtime, no dependencies: the server, its store and every client share one declaration. |
packages/client |
The floor over HTTP and its live socket, typed. What anything outside a floor reaches it through. |
packages/station |
The SDK a service station is written with: one function, and the claim loop around it. |
packages/pipeline |
A whole pipeline as one file, exported from or imported into a floor over HTTP. |
packages/lore-converter |
Reads a lore checkout and writes the definitions this floor runs in its place. |
deploy/chart |
One Helm chart, one image, both apps, toggled. |
.github/workflows |
CI on every pull request; build, push and deploy to GKE on main; a release on a v* tag. |
GitHub is not in this repository. lore owns it: lore posts GitHub's events to /events, runs a
service station for every GitHub action an assembly line takes, and answers floor's git-credential
request.
Floor's own GitHub app was deleted — see docs/decisions.md, "GitHub".
New here? Follow the tutorial — install floor with Helm, register a cluster to run agents in, import an assembly line, and start an assembly run of it. Five steps, each one ending in something you can check. To run one, docs/install.md installs, upgrades and uninstalls a release, and docs/using.md is the CLI, the client, the station SDK and the HTTP API, from the published packages.
Every release publishes the chart at
oci://ghcr.io/re-cinq/charts/floor, the image at ghcr.io/re-cinq/floor for linux/amd64 and
linux/arm64, and four npm packages, all under one version.
To work on floor itself:
npm install
npm run db:up && npm run db:migrate # Postgres on :5433
npm run build # before npm test: cross-package tests read dist
npm test && npm run typecheck && npm run lint
npm start # the dev loop, see docs/dev_loop.md
scripts/walk-*.sh walk a real run end to end against minikube.
docs/tutorial.md is the way in: installing, registering a cluster, importing an assembly line, and starting an assembly run of it. docs/README.md is the index and states the contract the pages keep: they describe the floor as it runs, anything designed but unwritten is marked Not built yet, and nothing unmarked should be false. Start with docs/assembly_run_storage.md for the model, docs/api_sketch.md for the endpoints, and docs/decisions.md for why any of it is the way it is.