Octov0.11.7
Extending

Extending Octo

The two extension points, and how to choose between them.

Everything above this section is about using Octo: writing flows in YAML. This section is about extending the runtime itself, in Go, with a connector so flows can talk to something new, or a runtime service so the process can do something new.

Two extension points

The runtime has exactly two.

connectorsservices
AnswersWhat a flow can doWhat the runtime is
Referenced byName, in YAMLNothing; it is not in the config
Exampleshttp, cron, database, slackstandalone, k8s, api, observability
Lives inruntime/connectors/<name>runtime/services/<name>
GuideConnectorsRuntime services

Both use package-level registration (init) and binary wiring via blank imports in runtime/octo/main.go. They differ in selection: service providers are module-selected by RUNTIME_SERVICES_MODULE, while hosted services run whenever they are compiled in and blank-imported. Nothing is discovered by scanning, and nothing is configured by file path.

New capability goes into one of these two, or, for a block that belongs to no connector, under runtime/blocks, registered through the same block seam a connector uses. Never into a third registry.

Choosing

Does a flow author reference it by name in YAML?
├─ yes → a connector, with its blocks, sources, and schema metadata
└─ no
   ├─ Does it supply platform capability the runtime already names
   │  (object store, queues, topics, leader election)?  → a services provider
   ├─ Does it run for the life of the process with
   │  its own CLI flags?                                → a hosted service
   ├─ Does it only need to watch what flows do?         → the event seams
   └─ Is it a block that owns no resource, and binds to
      any provider through a shared interface?          → runtime/blocks

Two things look like extension points but are not, and they are where many "I need to extend Octo" questions land.

The event seams, core.EventBus (one event per message, per flow) and core.BlockEvents (one pair per block invocation), let you observe the runtime without registering anything into it. Nothing you attach changes what a flow does. Reach for these first when the requirement is "know what happened"; see Monitoring. The observability service is built entirely on them and registers no connector and no block.

Blocks with sub-flows are ordinary registered blocks. fork, foreach, if, cache-scope and the ai-* family keep their sub-flows as fields of their settings struct and build them through core.BlockDeps.SubFlows, the seam the engine hands every block factory. A connector package can register one too; see Blocks with sub-flows. The engine owns no block of its own, only the debug wrappers the CLI injects.

Not extension points

CLI subcommands are a closed set: run, invoke, eval and schema. A hosted service extends the CLI by adding flags to octo run, which is the seam that exists.

CEL functions go through one seam, expr.RegisterMessageExtension, and all message expressions compile via expr.CompileMessage. Never compile at a call site.

Before you start

Read docs/coding-standards.md and docs/extension-points.md in the repository. The latter is the normative version of this page.

CI enforces one rule that surprises people: every registered connector, block and source must be documented. scripts/check-docs-drift.mjs compares octo schema against the octo_types frontmatter of the reference pages and fails on any drift.

On this page