Skip to content

Write your own scaffold

A scaffold is a choice of six components plus an optional bit of adaptation. There are two ways to build one: compose components inline (no new files), or subclass GalapagosScaffold and ship a card so others can load it by name.

Way 1 — build-your-own from components

Pass component instances (or import paths) straight to GalapagosScaffold.from_card. Any role you omit gets a sensible default; the Evaluator always comes from the task.

import galapagos as gx
from galapagos.components import (
    InMemoryPopulation, ExploreExploitPolicy, DefaultPromptBuilder, DiffProposer, NullMemory,
)

scaffold = gx.GalapagosScaffold.from_card(
    population=InMemoryPopulation(capacity=200),
    selection_policy=ExploreExploitPolicy(seed=0, explore_ratio=0.4, num_inspirations=3),
    prompt_builder=DefaultPromptBuilder(),
    proposer=DiffProposer(),
    memory=NullMemory(),
    model=gx.load_model("openai/gpt-4o-mini", host="openrouter"),
)

result = scaffold.run(task=gx.load_task("playground_sphere"), max_iterations=30)
print(result.best_score)

Each of population, selection_policy, prompt_builder, proposer, evaluator, memory may be:

  • a component instance (as above),
  • a dotted module.Class path (e.g. "galapagos.components.UCBBanditPolicy"), or
  • a .py file path containing exactly one subclass of the right base class.

This is the fastest way to prototype: swap one slot (say, UCBBanditPolicy for ExploreExploitPolicy) and re-run. See the six components for every shipped implementation.

Way 2 — subclass + register + ship a card

For a reusable method, subclass GalapagosScaffold, override build_components, and register it. This is exactly how the bundled OpenEvolveScaffold and AdaEvolveScaffold are built.

The class

# my_scaffold/scaffold.py
from galapagos.scaffolds import GalapagosScaffold, register_scaffold
from galapagos.components import (
    IslandPopulation, UCBBanditPolicy, DefaultPromptBuilder, DiffProposer, ScratchpadMemory,
)
from galapagos.config import GalapagosConfig
from galapagos.models import GalapagosModel
from galapagos.records import Genome


@register_scaffold("banditevolve")
class BanditEvolveScaffold(GalapagosScaffold):
    name = "banditevolve"

    @classmethod
    def build_components(cls, config: GalapagosConfig, model: GalapagosModel | None) -> dict:
        """Return the five scaffold-side components. The Evaluator comes from the task."""
        seed = int(config.get("seed", 0))
        n = config.get("database.num_islands", 4)
        return {
            "population": IslandPopulation(num_islands=n, migration_interval=20),
            "selection_policy": UCBBanditPolicy(seed=seed, num_islands=n,
                                                c=config.get("selector.ucb_c", 1.4)),
            "prompt_builder": DefaultPromptBuilder(include_memory=True),
            "proposer": DiffProposer(),
            "memory": ScratchpadMemory(max_notes=config.get("memory.max_notes", 8)),
        }

    # ---- optional adaptation hooks (no-ops by default) ----
    def before_step(self) -> None:
        """Runs before selection each step — adjust intensity, ε, mode, ..."""

    def after_step(self, child: Genome, result) -> None:
        """Runs after a child is evaluated (result is None on a no-op step).
        Here: periodically distill the best feedback into the meta-scratchpad."""
        interval = self.config.get("memory.write_interval", 10)
        if result is None or self.state.iteration % interval != 0:
            return
        best = self.state.best
        feedback = best.artifacts.get("text_feedback") if best else None
        if feedback:
            self.memory.write(f"iter {self.state.iteration}: best={best.fitness:.4f}{feedback}")

    def periodic(self) -> None:
        """Runs once per iteration, after the step — strategy refresh, migration triggers, ..."""

@register_scaffold("banditevolve") wires the class to its card name and adds it to registered_scaffolds(). Importing the module is enough to register it.

The hooks

The base loop is fixed; subclasses adapt through three no-op hooks called each step:

Hook When Typical use
before_step() before selection mode switching, intensity / ε schedules
after_step(child, result) after the child is evaluated (result is None on a no-op) bandit credit, stagnation response, memory writes
periodic() once per iteration, after the step meta-scratchpad refresh, strategy co-evolution, migration

You also have the full run state on self: self.state (a RunState: iteration, cost_usd, best, signals), self.population, self.memory, self.config, self.model, and self._stale (iterations since the last best-score improvement).

The card

Ship a card.yaml with your package so the method is loadable by card; the controller field points at your class. (Bundled scaffolds keep theirs next to the code, at src/galapagos/scaffolds/<name>/card.yaml; for your own package any path works — you load it with from_card(path=...).)

# my_scaffold/card.yaml
name: banditevolve
display_name: BanditEvolve
type: test_time_search
tier: search
status: stable
summary: "UCB-routed island evolution with a meta-scratchpad."
description: |
  A bandit SelectionPolicy routes the budget toward the most rewarding islands; a meta-scratchpad
  Memory accumulates distilled design insights.
source: "your repo / paper"
license: Apache-2.0
controller: my_scaffold.scaffold.BanditEvolveScaffold
components:
  population: {kind: island}
  selection_policy: {kind: ucb_bandit}
  prompt_builder: {kind: default_with_memory}
  proposer: {kind: diff}
  evaluator: {kind: task}
  memory: {kind: scratchpad}
model:
  default: openai/gpt-5.5
  host: openrouter
  roles: [propose]
requirements: {gpu: none, docker: optional, python: ">=3.10"}
defaults_config: config.yaml

The config

Ship a config.yaml with the defaults your build_components reads. It lives next to the code (the bundled scaffolds keep theirs at src/galapagos/scaffolds/<name>/config.yaml); when bundled it loads via GalapagosConfig.from_config(scaffold_name="banditevolve"), otherwise via path=:

# my_scaffold/config.yaml
seed: 0
budget:
  max_iterations: 100
  patience: null
database:
  num_islands: 4
  migration_interval: 20
selector:
  ucb_c: 1.4
memory:
  max_notes: 8
  write_interval: 10

Run it

import my_scaffold.scaffold        # the @register_scaffold decorator fires on import
import galapagos as gx

scaffold = gx.GalapagosScaffold.from_card(
    "banditevolve",
    config=gx.GalapagosConfig.from_config(path="my_scaffold/config.yaml"),
    model=gx.load_model("openai/gpt-4o-mini", host="openrouter"),
)
result = scaffold.run(task=gx.load_task("playground_sphere"), max_iterations=40)
print(result.summary["scaffold"])     # -> "banditevolve"

gx.available_scaffolds() and galapagos scaffold list show the cards bundled in the galapagos package (under src/galapagos/scaffolds/); a third-party scaffold loads by explicit path (from_card(path="my_scaffold/card.yaml")) or, once its module is imported (or registered via the galapagos.scaffolds entry-point group), by its @register_scaffold name. To submit it to the Hub, see Submit to the Hub.