Skip to content

Build Your First Broadcast-Orchestrated Substrate

This tutorial walks you through building a new substrate package in the same style as a CliSensoryCortexSubstrate.

You will build:

  1. A package boundary launch function with the canonical signature.
  2. A RAS-driven orchestration path that requests the modulators your substrate needs.
  3. A synthesizer that satisfies one of those requests stores the modulator in Neuromodulators and returns a ModulatorSuccess child result with an artifact-ref.
  4. A concrete substrate type that satisfies the substrate contract.

You should already know how to run Python tests in your repo and use uv.

Required concepts:

  1. Substrate domain contracts from cerebel-substrate-core.
  2. Domain bundle contracts from cerebel-contracts.

Create a new package with a launch entrypoint, a substrate module, and a synthesizer module:

src/my_new_substrate/
__init__.py
launch.py
synthesizers.py
substrate.py

In __init__.py, export launch:

from .launch import launch
__all__ = ["launch"]

Implement a concrete substrate class that inherits from Substrate and reads the injected cocktail:

from __future__ import annotations
from cerebel_substrate_core import (
ModulatorCocktail,
Substrate,
SubstrateHandle,
SubstrateLifecycleState,
)
class CliSensoryCortexSubstrate(Substrate):
def __init__(self, *, context: ModulatorCocktail) -> None:
super().__init__(definition_id=context.domain.definition_id)
self._context = context
@property
def cortex_role(self) -> str:
return self._context.modulators["cortex_role"]
def handle(self) -> SubstrateHandle:
return SubstrateHandle(
definition_id=self.definition_id,
instance_id=self.instance_id,
healthy=self.health(),
lifecycle_state=SubstrateLifecycleState.READY,
)

Model your synthesizer after the biological factory pattern:

  1. RAS submits a request to Neuromodulators.
  2. The synthesizer decides whether it can satisfy that request.
  3. If so, it creates the modulator, stores it in Neuromodulators, and returns a ModulatorSuccess with artifact-ref.

Minimal example:

from __future__ import annotations
from cerebel_substrate_core import Modulator
from neural_relay import ModulatorRequest, ModulatorResult, ModulatorSuccess, Synthesizer
class CortexLaminaSynthesizer(Synthesizer):
def __init__(self, neuromodulators: NeuromodulatorsForSynthesizer) -> None:
self._neuromodulators = neuromodulators
def can_handle(self, modulator_name: str) -> bool:
return modulator_name == "cortex_role"
def synthesize(self, request: ModulatorRequest) -> ModulatorResult:
modulator = Modulator(name="cortex_role", value="sensory")
artifact_ref = self._neuromodulators.add("cortex-role", modulator)
return ModulatorSuccess(
request_id=request.payload.request_id,
synthesizer_id="cortex-lamina-synthesizer",
artifact_ref=artifact_ref,
)

Your package boundary should expose:

def launch(domain: SubstrateDomain) -> Substrate:
...

Example wiring:

from __future__ import annotations
from cerebel_substrate_core import ModulatorCocktail, Substrate, SubstrateDomain
from neural_relay import ModulatorRequest, RAS
from cerebel_runtime import Neuromodulators
from .synthesizers import CortexLaminaSynthesizer
from .substrate import CliSensoryCortexSubstrate
def launch(domain: SubstrateDomain) -> Substrate:
neuromodulators = Neuromodulators()
neuromodulators.register_synthesizer(CortexLaminaSynthesizer(neuromodulators))
ras = RAS(neuromodulators=neuromodulators)
cocktail = ModulatorCocktail(domain=domain)
response = ras.request_modulator(ModulatorRequest(modulator_name="cortex_role"))
role = neuromodulators.get(response.payload.artifact_ref)
cocktail = cocktail.update_modulator(role.name, role.value)
return CliSensoryCortexSubstrate(context=cocktail)

Define a cortex entry in your Domain Bundle with launch target metadata:

kind: cortex
definition-id: comm.cli
role: comms
description: CLI comms cortex
lifecycle-policy: dynamic
launch:
launcher: inprocess
target:
module-path: my_new_substrate
symbol-name: launch

The launcher resolves this metadata and calls launch(domain).

Write tests for three boundaries:

  1. Contract tests: launch returns a Substrate-compatible substrate instance.
  2. Modulation tests: the expected synthesizers satisfy the expected requests.
  3. Failure tests: missing synthesizers or unsatisfied requests fail clearly.

The expected call flow:

sequenceDiagram
    participant Caller
    participant Launcher
    participant Adapter
    participant Pkg as launch(domain)
    participant RAS
    participant Neuromodulators
    participant Synthesizer
    participant Terminal
    participant Relay
    participant Cocktail
    participant Substrate

    Caller->>Launcher: launch(domain)
    Launcher->>Adapter: dispatch
    Adapter->>Pkg: invoke
    Pkg->>RAS: register Synthesizers
    Pkg->>RAS: request_modulator(cortex_role)
    RAS->>Neuromodulators: ModulatorRequest
    Neuromodulators->>Synthesizer: fanout ModulatorRequest
    Synthesizer->>Neuromodulators: add(modulator)
    Neuromodulators-->>RAS: ModulatorSuccess(artifact-ref)
    RAS->>Neuromodulators: get(artifact-ref)
    RAS->>Terminal: send transmission
    Terminal->>Relay: publish transmission
    Relay-->>Terminal: distribute transmission
    Terminal-->>Cocktail: modulator arrives
    Cocktail-->>Substrate: configure runtime
    Substrate-->>Pkg: substrate instance
    Pkg-->>Adapter: substrate instance
    Adapter-->>Launcher: substrate instance
    Launcher-->>Caller: substrate instance

You now have the same high-level composition shape used by the current Cerebel architecture:

  1. One launch entrypoint.
  2. Neuromodulators-mediated RAS orchestration.
  3. Synthesizer-driven modulator production.
  4. Substrate contract compliance.

Next step: follow How to Build a New Substrate Package for production hardening and release checklist guidance.