Go runtime YAML flows CEL expressions hot reload v0.1.4

The integration platform,
reimagined for what comes next.

Build enterprise integration flows in a language you already know — then let a modern, concurrent runtime carry them. Cloud-native today, AI-native by design.

greet.yaml
Why this exists

After ten years building integration software, I wanted to see how a cloud-native — and later, AI-native — integration platform would really feel. So I dared to dream of a better, modern design that could survive the needs of a new generation of infrastructure.

Structurally it stays familiar: you compose flows in a readable language, and every enterprise integration pattern you rely on is here. But the engine underneath is new — built-in hot reload to make development a joy, flows you can call straight from the CLI to make testing trivial, and a clean concurrency model at its core.

With this simple ESB working, the foundation is set. Now the real adventure begins: making it Kubernetes-native and AI-native.

— a lab, an experiment, and a love letter to integration. Many more features to come.
What's in the box

A small, sharp runtime — with the ergonomics you wished for.

Everything you need to model real integrations, and nothing that gets in your way.

🧩

Familiar flow language

Declare connectors, flows, and processors in YAML. A flow is an ordered list of blocks — and a flow is itself a processor, so composition is recursive.

♻️

Hot reload

Run with --watch and the runtime rebuilds on every config or .env change. Edit a flow, save, watch it take effect — no restart, no ceremony.

Call flows from the CLI

invoke runs a single flow by name with JSON in and JSON out — no sources started, no ports bound. Testing a flow is one command.

🧵

Concurrent by design

Each flow runs on a dedicated worker pool reading a bounded channel. Backpressure is built in; FIFO is one setting away (workers: 1).

🔤

CEL expressions

Routing, transforms, and payloads are CEL expressions, compiled once at build time so a typo fails at startup, not in production.

📡

Observable

Every message emits started then exactly one terminal event — completed, dropped, or failed — on a process-wide event bus for metrics and dead-lettering.

The processing model

From an external event to a processed message.

A connector turns the outside world into messages; a source feeds them to a flow; the flow runs them through an ordered chain of blocks. That's the whole model — and it composes all the way down.

flowchart LR EXT([external event]):::ext --> CONN["Connector
owns connections, tx, clients"]:::conn CONN --> SRC["Message Source
builds a Message"]:::src SRC --> CH{{"bounded channel
(buffer)"}}:::ch CH --> FLOW subgraph FLOW["Flow — a MessageProcessor"] direction LR B1["block"]:::blk --> B2["block"]:::blk --> B3["block"]:::blk end FLOW --> OUT([terminal event]):::ext classDef ext fill:#11161d,stroke:#36d399,color:#e6edf3; classDef conn fill:#141b24,stroke:#4aa8ff,color:#e6edf3; classDef src fill:#141b24,stroke:#4aa8ff,color:#e6edf3; classDef ch fill:#1a232e,stroke:#f5b14c,color:#e6edf3; classDef blk fill:#141b24,stroke:#b692ff,color:#e6edf3;
connector → source → flow → block → processor
flowchart TB SRC["Source"]:::src --> CH{{"buffer
bounded channel"}}:::ch CH --> W1["worker 1"]:::w CH --> W2["worker 2"]:::w CH --> W3["worker N"]:::w W1 --> RUN["run flow
per message"]:::run W2 --> RUN W3 --> RUN RUN -. "fork branches" .-> SP[("shared flow pool")]:::pool classDef src fill:#141b24,stroke:#4aa8ff,color:#e6edf3; classDef ch fill:#1a232e,stroke:#f5b14c,color:#e6edf3; classDef w fill:#141b24,stroke:#36d399,color:#e6edf3; classDef run fill:#141b24,stroke:#b692ff,color:#e6edf3; classDef pool fill:#1a232e,stroke:#b692ff,color:#e6edf3;
Two levels of concurrency: a per-flow worker pool, plus a shared pool for parallel composites. Backpressure comes from the bounded channel.
stateDiagram-v2 direction LR [*] --> started started --> completed: returns a message started --> dropped: returns (nil, nil) started --> failed: returns an error completed --> [*] dropped --> [*] failed --> [*]
Each message produces a started event, then exactly one terminal event on the flow-event bus.
How the container works

One process, one container, a clear lifecycle.

A service owns the start/stop discipline: connectors acquire resources first, flows build and start, sources begin to emit. On shutdown everything unwinds in strict reverse — channels close, workers drain, connectors release.

flowchart TB subgraph SVC["Octo service · one process / one container"] direction TB subgraph CONN["Connectors — acquire resources on start"] direction LR C1["cron"]:::c C2["http"]:::c C3["http-client"]:::c C4["database"]:::c C5["logger"]:::c end subgraph FL["Flows — concurrent message processors"] direction LR F1["orders-api
http source"]:::f F2["enrich-order
sourceless · callable"]:::f end end C2 -->|"provides source"| F1 F1 -->|"flow-ref (sync)"| F2 C4 -. "sql block" .-> F1 C5 -. "log block" .-> F1 classDef c fill:#141b24,stroke:#4aa8ff,color:#e6edf3; classDef f fill:#141b24,stroke:#36d399,color:#e6edf3;
Connectors own external resources and may provide a source to a flow. Blocks reach back into named connectors for capabilities (a logger, a DB, an HTTP client).
sequenceDiagram autonumber participant S as Service participant C as Connectors participant F as Flows participant Src as Sources S->>C: Start (acquire resources, in order) S->>F: Build (resolve source, build block chain) S->>F: Start worker pool + shared pool F->>Src: Start — begin emitting messages Note over S,Src: running — messages flow through workers S-->>Src: ctx.Done() → Stop source Src-->>F: close channel, drain workers F-->>C: Stop connectors (strict reverse order)
The start/stop lifecycle. Whoever creates the channel closes it — after the source stops.

Composite blocks — composition all the way down

A composite block embeds sub-flows through explicit, typed slots. handle-errors is the single-threaded half (an error boundary with recovery); fork is the concurrent half (scatter, then join). Both keep the one-in / one-out seam intact.

flowchart TB IN([message]):::ext --> MAIN subgraph SCOPE["handle-errors — error boundary · sequential"] direction TB MAIN["process chain"]:::blk -->|ok| OUT MAIN -->|error| ALT["error chain
sees vars.error · recover"]:::alt --> OUT([continue]):::ext end classDef ext fill:#11161d,stroke:#36d399,color:#e6edf3; classDef blk fill:#141b24,stroke:#b692ff,color:#e6edf3; classDef alt fill:#141b24,stroke:#f5b14c,color:#e6edf3;
handle-errors — try / catch, the recovery path.
flowchart TB IN([message]):::ext --> FORK{{fork}}:::fork FORK -->|clone| A["branch: notify"]:::blk FORK -->|clone| B["branch: audit"]:::blk FORK -->|clone| C["branch: index"]:::blk A --> JOIN{{join}}:::fork B --> JOIN C --> JOIN JOIN --> OUT([input passes through]):::ext classDef ext fill:#11161d,stroke:#36d399,color:#e6edf3; classDef fork fill:#1a232e,stroke:#4aa8ff,color:#e6edf3; classDef blk fill:#141b24,stroke:#b692ff,color:#e6edf3;
fork — scatter-gather on the shared pool, joins before returning.

The platform — native on Kubernetes

That runtime is one process. The platform around it is cloud-native: a control plane — the editor (with OIDC SSO) and the orchestrator, backed by Postgres — and a data plane where every integration you deploy becomes its own Kubernetes Deployment of the very same runtime image. Replicas are pods; a Service gives each a stable in-cluster address; an Ingress makes it public when you ask. See the deployment guide.

flowchart TB USER([you · browser]):::ext --> ED subgraph CP["Control plane"] direction LR ED["Editor
Next.js · OIDC SSO"]:::conn -->|BFF| OR["Orchestrator"]:::conn OR --> PG[("Postgres
integrations · deployments · secrets")]:::pool end OR -->|"applies manifests"| K8S{{"Kubernetes API"}}:::ch subgraph DP["Data plane — one Deployment per integration"] direction LR P1["pod
octo runtime + your flows"]:::f P2["pod
octo runtime"]:::f end K8S --> P1 K8S --> P2 NET([public traffic]):::ext --> ING["Traefik Ingress
slug.your-domain"]:::src ING --> SVC["Service
octo-int-slug"]:::src SVC --> P1 SVC --> P2 classDef ext fill:#11161d,stroke:#36d399,color:#e6edf3; classDef conn fill:#141b24,stroke:#4aa8ff,color:#e6edf3; classDef src fill:#141b24,stroke:#f5b14c,color:#e6edf3; classDef f fill:#141b24,stroke:#36d399,color:#e6edf3; classDef ch fill:#1a232e,stroke:#b692ff,color:#e6edf3; classDef pool fill:#1a232e,stroke:#4aa8ff,color:#e6edf3;
The iPaaS, native on Kubernetes: the editor + orchestrator + Postgres control plane provisions one Deployment per integration; pods run the same runtime, behind a Service and an optional Ingress.
Enterprise integration patterns

The patterns you know — mapped to real blocks.

Many classic EIPs map onto a block, a composite, or a connector capability that ships today. Every entry below names a construct you can actually write in a flow — nothing aspirational.

PatternIn Octo
MessageJSON body + vars + correlationID
Polling Consumercron source
Request–Replyhttp source (synchronous response)
Content-Based Routerswitch block
Conditional branchif block
Message Translatorset-payload (CEL) · ai-mapping (LLM)
Content Enricherflow-ref (synchronous; result folds back)
Splitterforeach block
Scatter-Gatherfork composite
Wire Taplog block (passes the message through)
Dead Letter / recoveryhandle-errors block · flow error: chain
Process Managersourceless flow + flow-ref
Content-based routing (LLM)ai-router composite
flowchart TB IN(["HTTP POST /orders/{id}"]):::ext --> WT["log · wire tap"]:::blk WT --> AUD[/"flow-ref: audit
one-way"/]:::async WT --> SW{"switch
content-based router"}:::route SW -->|"method == POST"| ENR["flow-ref: enrich-order
content enricher"]:::blk SW -->|"method == GET"| LK["set-payload
translator"]:::blk SW -->|default| ERR["set-payload: error"]:::alt ENR --> RESP([JSON 200]):::ext LK --> RESP ERR --> RESP classDef ext fill:#11161d,stroke:#36d399,color:#e6edf3; classDef blk fill:#141b24,stroke:#b692ff,color:#e6edf3; classDef route fill:#1a232e,stroke:#4aa8ff,color:#e6edf3; classDef async fill:#141b24,stroke:#f5b14c,color:#e6edf3; classDef alt fill:#141b24,stroke:#ff6b6b,color:#e6edf3;
The http-orders sample, drawn: a wire tap, a one-way audit, content-based routing, and a synchronous content enricher.
Show, don't tell

Runnable samples.

Every example below ships in the repo under samples/ and runs with zero setup via task run:sample. Pick one and read the YAML.

Go deeper

Guides.

Beyond the landing tour: reference pages for the pieces you'll reach for most. More to come.

Quickstart

Running in under a minute.

1 · Clone & build

# requires Go 1.25+ and Task
git clone https://github.com/juancavallotti/octo
$ cd octo && task build

2 · Run a sample

$ task run:sample -- hello-world.yaml

A cron tick logs a greeting every 30 seconds.

3 · Call a flow directly (no ports, no sources)

$ ./bin/octo invoke \
  --config samples/hello-invoke.yaml --flow greet \
  --data '{"name":"Ada"}'
# → {"greeting":"hello, Ada!"}

4 · Develop with hot reload

$ ./bin/octo --config samples/http-orders.yaml --watch

Edit the YAML (or a .env), save, and the runtime rebuilds itself.

5 · Check the version

$ ./bin/octo --version
# → octo 0.1.6 (built 2026-06-18T22:31:23Z)
Tip: sourceless flows (no source:) get an implicit source and become callable by name — ideal for invoke and for flow-ref sub-flows.
Recently shipped

What's new.

The latest releases brought AI blocks, single sign-on for the editor, first-class error handling, and a friendlier developer console. Here's the tour.

🤖

AI blocks & LLM connectors

Connect Anthropic, OpenAI, or Gemini, then drop in AI blocks: ai-router (an LLM picks a route), ai-agent (calls your sub-flows as tools), ai-mapping (reshapes a payload to a schema), and ai-retry (self-healing recovery).

🔐

Single sign-on for the editor

The visual editor now supports OIDC SSO (Auth.js). Backend routes are role-gated from an id-token claim, sessions are stateless JWTs, and credentials moved out of Secret Manager. SSO is opt-in — local task dev runs stay unauthenticated.

🧯

First-class error handling

Wrap steps in a handle-errors block for inline recovery, or give a flow a sibling error: chain. The recovery path sees vars.error.{message, flow, block}, and vars.httpStatus sets the response code. See the error-handling sample.

🖥️

A friendlier dev console

The editor's bottom panel is now tabbed — Logs alongside a Dev .env editor for local secrets and variables — and a run's test URL is surfaced right in the log panel so you can hit your endpoint in one click.

An ai-router flow on the Octo editor canvas: an AI Router block fanning out to billing, technical, and default routes.
An ai-router flow rendered in the visual editor.
The error-handling sample on the canvas: a handle-errors block with a recovery lane, and a flow-level error path.
A flow's recovery path: the error: chain that sees vars.error.
The vision

An AI-native iPaaS, running on Kubernetes — today.

The cloud-native ESB, the AI blocks, and the Kubernetes-native platform are all shipping now. Focus turns next to clustering, stateful primitives, and a richer integration lifecycle.

flowchart LR A["Shipping now
cloud-native ESB · AI blocks
Kubernetes-native platform"]:::now A --> B["Next
clustering · KV state
integration versioning · MCP"]:::next B --> C["Later
autoscaling · operator & CRDs
GitOps · deeper agents"]:::later classDef now fill:#0f2a20,stroke:#36d399,color:#e6edf3; classDef next fill:#11212e,stroke:#4aa8ff,color:#e6edf3; classDef later fill:#1a142e,stroke:#b692ff,color:#e6edf3;
● Now — shipping

ESB · AI · Platform

  • YAML flows, recursive composition & the EIP blocks (switch · if · foreach · fork · handle-errors)
  • cron · http · http-client · database · logger connectors; hot reload & CLI invoke
  • AI blocks: Anthropic / OpenAI / Gemini + ai-router · ai-agent · ai-mapping · ai-retry
  • Visual editor + orchestrator that deploy integrations as Kubernetes pods
  • OIDC SSO, public ingress & automatic TLS
◇ Later

Scale & ecosystem

  • Autoscaling on backpressure
  • An operator and flow CRDs
  • GitOps-friendly deployments
  • Health, readiness & metrics endpoints
  • Deeper agentic composition & more connectors
This is a lab and an experiment — a dream of what a modern integration platform could be. Expectations welcome; many more features to come.
Releases

What's new.

Versioning and release notes are automated with release-please from Conventional Commits. The current docs build reflects v0.1.6, kept in sync automatically on every release.

Loading the latest changelog from GitHub…

All releases ↗ Full changelog ↗