Skip to content

description: The GenSwarms swarm configuration DSL: declare agents, deterministic objects, topology edges, and backends in .exs, .json, or .yaml.

Swarm configuration

The Swarm Configuration DSL is the declarative format that defines a swarm: its name, its agents and objects, and the topology that connects them. Configs can be written as Elixir (.exs), JSON (.json), or YAML (.yaml/.yml) and are validated by Genswarms.Config.SwarmConfig when loaded.

Top-level structure

A configuration is a map with the following keys.

Key Type Required Description
name string or atom Yes Unique swarm identifier. Must start with a letter and contain only alphanumerics, _, or -. Atoms are converted to strings.
agents list of maps Yes One or more agent definitions. Must be non-empty.
objects list of maps No Non-agentic Elixir/backend components. Defaults to [].
topology list of {from, to} tuples No Directed communication edges. Defaults to [].
skills_base_dir string No Base directory to resolve skill files from. Stored on the parsed struct; not otherwise validated.
options map No Free-form additional settings. Defaults to %{}.
%{
  name: "example-swarm",
  agents: [
    %{name: :researcher, backend: :local, skills: ["web.md"]},
    %{name: :coder, backend: {:docker, "coder"}, skills: ["code.md"]}
  ],
  objects: [],
  topology: [
    {:researcher, :coder},
    {:coder, :researcher}
  ]
}

Agent configuration

Each entry in agents is a map. Only the keys below are recognized by the validator and serializer; unknown keys are passed through into the agent map but are not validated.

Key Type Required Default Description
name atom or string Yes Unique agent identifier. Strings are normalized to atoms.
backend backend spec No :bwrap Where/how the agent runs (see Backend value forms). If omitted, the parser fills in :bwrap.
model string No LLM model (e.g. provider/model-name). Passed through to subzeroclaw as SUBZEROCLAW_REQUEST_EXTRA = {"model": …}. There is no SUBZEROCLAW_MODEL env fallback (it is a dead variable). When unset, subzeroclaw uses its own default. For router routing, set request_extra (a policy_ir) instead of model.
endpoint string No API endpoint URL. When unset, the backend passes through SUBZEROCLAW_ENDPOINT; otherwise subzeroclaw auto-detects from the API key.
skills list of strings No [] Skill markdown filenames to deploy. All entries must be strings.
presets list of atoms No [] NixOS tool presets. Must be drawn from the valid preset set below.
tools list of atoms No [] Individual tools. Must be drawn from the valid tool set below.
config map No %{} Backend-specific and domain-specific configuration (see Backend config separation).
%{
  name: :researcher,
  backend: :local,
  model: "anthropic/claude-sonnet-4",
  endpoint: "https://openrouter.ai/api/v1/chat/completions",
  skills: ["web.md", "summarize.md"],
  presets: [:base, :web]
}

Valid presets

Presets are validated against the @valid_presets set in Genswarms.Config.SwarmConfig (the corresponding NixOS package sets live in nix/tool-presets.nix):

:base, :web, :code, :python, :node, :data, :docs, :network, :system, :security, :containers, :cloud, :ai

A presets value that is not a list of atoms fails with :invalid_presets_format.

Valid tools

Individual tools are validated against the complete @valid_tools set in Genswarms.Config.SwarmConfig:

:git, :curl, :wget, :jq, :yq, :tree, :htop, :ripgrep, :rg, :fd, :fzf, :ag, :vim, :neovim, :nano, :python, :python3, :node, :nodejs, :ruby, :go, :rustc, :cargo, :make, :cmake, :gcc, :clang, :sqlite, :postgresql, :mysql, :redis, :duckdb, :pandoc, :pdftotext, :ssh, :rsync, :netcat, :httpie, :docker, :podman, :kubectl, :gh, :glab, :miller, :csvkit, :xsv, :ffmpeg, :imagemagick, :pytest, :ruff, :mypy, :black, :flake8, :pip, :poetry, :uv.

A tools value that is not a list of atoms fails with :invalid_tools_format. Atoms outside the set above fail with {:unknown_tools, invalid} (where invalid is the list of offending atoms); likewise unknown presets fail with {:unknown_presets, invalid}.

Backend value forms

The backend key accepts any of the following forms. See backends.md for behavioral details.

Form Backend Notes
:local Local Runs as an Elixir Port subprocess.
{:docker, "name"} Docker Container image/name; resolves to %{image: "name"}.
{:docker, "name", %{opts}} Docker Options map merged over %{image: "name"}.
:apple_container Apple container Apple container CLI backend; image is picked from presets/defaults.
{:apple_container, "image"} Apple container Container image; resolves to %{image: "image"}.
{:apple_container, "image", %{opts}} Apple container Options map merged over %{image: "image"}.
{:ssh, "user@host"} SSH Resolves to %{host: "user@host"}.
{:ssh, "user@host", %{opts}} SSH Options map merged over %{host: ...}.
{:tmux, :codex} Tmux TUI Persistent interactive Codex pane; :claude and :opencode are also supported. Runs on the host unless a runner is selected.
{:tmux, client, %{opts}} Tmux TUI Persistent pane with host, per-agent Docker, or per-agent bwrap execution.
:bwrap Bwrap Bubblewrap sandbox; default when backend is omitted.
{:bwrap, %{opts}} Bwrap Options map (see below).
:mock Mock No process; a stub for testing.
{:mock, %{script: [...]}} Mock script is stored for test introspection only — the backend does not generate responses (see backends.md).

Any other value fails validation with {:invalid_backend, backend}.

JSON/YAML limitation: the loader normally converts only a scalar string backend to an atom ("local":local, "bwrap":bwrap, etc.). It does not turn an array like ["docker", "coder"] into a tuple, so Docker/SSH/Apple tuple forms still require .exs. Tmux is the explicit exception and accepts a data-only object: {"type":"tmux","client":"codex","opts":{...}} (the same shape works in YAML).

Backend config separation

For agents, the agent's config map is split at deploy time (in Genswarms.Agents.AgentServer) into backend keys and domain keys. Backend keys override the values returned by SwarmConfig.backend_config/1 and control the execution environment; all remaining keys stay as domain config available to the agent's skills and logic.

The shared/backend-specific keys are:

Key Type Default Description
workspace string /tmp/szc-workspace/<sandbox_id> Working directory mounted read-write into the sandbox.
container_name string szc-<swarm>-<agent> Explicit Apple container name.
env map %{} Extra environment variables for Docker/Apple container agents.
volumes list of {host, container} [] Extra Docker/Apple container bind mounts.
cmd string or argv list backend default Override the Docker/Apple in-container command.
extra_path list of strings [] Additional directories prepended to PATH.
extra_ro_binds list of {host, container} [] Extra read-only bind mounts.
extra_rw_binds list of {host, container} [] Additional writable mounts for tmux Docker/bwrap runners. The subzeroclaw bwrap backend does not currently apply this key; use workspace there.
extra_env map %{} Extra environment variables passed into the sandbox.
memory_limit string subzeroclaw bwrap: "256M"; tmux bwrap rootless: unset; tmux bwrap cgroup: "2G" Memory ceiling (e.g. "2G"). Rootless uses RLIMIT_AS; JS runtimes can reserve large virtual heaps, so set it deliberately.
memory_swap string Docker default Docker-only RAM+swap cap.
cpu_limit number/string backend default Docker/Apple CPU cap.
cpu_shares integer 100 Relative CPU weight.
pids_limit integer Docker default Docker-only process-count cap.
tasks_max integer 50 Max number of tasks/processes.
subzeroclaw_path string resolved Explicit path to the subzeroclaw binary.
subzeroclaw_src string resolved Source directory mounted into Docker/Apple containers for in-container build.
api_key / model / endpoint string env/provider defaults LLM settings passed through to real backends. Prefer top-level model / endpoint for normal configs.
request_extra / compact_extra JSON string or map none Advanced subzeroclaw request/compaction routing payloads.
presets list of atoms [:base] The same agent-level presets key (above), forwarded to the sandbox. The bwrap backend falls back to [:base] when none are given.
network :open | :none | :isolated | string :open Subzeroclaw Docker/bwrap use :isolated for LLM-only egress. An isolated tmux TUI runner supports :none (no network); Docker also accepts a named network. Tmux TUI runners reject :isolated until their clients have a safe LLM-forwarding contract. Apple container also rejects :isolated. See Security › network isolation.
client / resume tmux client / boolean or string required / false Select a coding TUI and optionally resume its latest/named conversation.
runner :host | :docker | :bwrap :host Where a tmux pane executes its coding client. tmux itself remains host-side and attachable.
client_source :runtime | :host_nix Docker: :runtime; bwrap: :host_nix Use a client already in the isolated runtime, or mount the selected host Nix closure read-only.
privilege_mode :rootless | :cgroup Tmux bwrap: :rootless; subzeroclaw bwrap: :cgroup TUI bwrap defaults rootless so the client keeps a direct PTY; select cgroup explicitly for systemd-enforced limits on a compatible host.
image / state_dir string / string preset image / private per-agent path Docker TUI image and persistent client home mounted as /root.
pass_env / runner_env list of names / map [] / %{} Environment passed into an isolated TUI runner. Docker and bwrap use private mode-0600 env files so values are not placed in host-visible CLI argv.
keepalive_command argv list ["sleep", "infinity"] Command that keeps a per-agent Docker TUI container alive between client restarts.
docker_executable / bwrap_executable string resolved from PATH Isolation CLI overrides for a tmux runner.
tmux_socket / tmux_executable string genswarms / tmux Persistent tmux transport selection.
executable / args string / list of strings client default / [] Tmux client executable and extra argv; values are never interpolated into a host shell.
approval_policy / sandbox string/atom client default Codex permissions.
permission_mode / effort string/atom client default Claude permissions and effort.
dangerously_bypass / auto_approve boolean false Explicit unsafe client modes; never enabled by default.
poll_interval_ms / ready_quiet_ms positive integer 250 / 1000 Tmux observation timing. Quiet readiness is ignored unless quiet_ready_fallback: true.
submit_delay_ms / submit_retry_after_ms non-negative / positive integer 100 / 1000 Separates literal input from the first Enter, then bounds the active-cursor verification grace.
submit_max_attempts / submit_check_max_errors positive integer 2 / 3 Bounded Enter-only recovery and cursor-query failures before needs_attention.

For example, this JSON shape creates an attachable OpenCode pane backed by its own Docker container; the image must contain opencode:

{
  "type": "tmux",
  "client": "opencode",
  "opts": {
    "runner": "docker",
    "image": "coding-tuis:latest",
    "client_source": "runtime",
    "network": "open"
  }
}

On NixOS, use "client_source": "host_nix" to mount the installed client's minimal Nix closure instead. network: "none" is a complete network cutoff and therefore also blocks cloud model APIs; it is not equivalent to the subzeroclaw-only network: "isolated" LLM forwarder.

Any key in config not listed above (for example population_size or max_iterations) is preserved as domain config and is not interpreted by the backend.

%{
  name: :fixer,
  backend: :bwrap,
  config: %{
    # Backend keys
    workspace: "/tmp/workspace",
    extra_path: ["/opt/tools/bin"],
    extra_ro_binds: [{"/home/user/project", "/project"}],
    memory_limit: "512M",
    # Domain keys
    population_size: 10,
    max_iterations: 50
  }
}

Objects

Objects are non-agentic components that participate in topology but run deterministic code instead of LLM calls. Each object must specify either a handler (native Elixir) or a backend (Docker/Apple container/SSH). See objects.md for the handler behaviour.

Key Type Required Description
name atom or string Yes Unique object identifier. Normalized to an atom.
handler module For native objects Module implementing init/1 and handle_message/3 from Genswarms.Objects.ObjectHandler.
backend backend spec For Docker/Apple container/SSH objects Same forms as agent backends.
config map No Passed to the handler's init/1 (or to the backend).

If a handler module is already loaded, the validator checks it exports init/1 and handle_message/3 (raising {:invalid_handler, handler, ...} otherwise); if the module is not yet loaded (it may live in the host application), validation is deferred. An object map with neither handler nor backend fails with :invalid_object_config.

objects: [
  %{
    name: :evaluator,
    handler: MyApp.Objects.Evaluator,
    config: %{parallel: true, timeout: 300_000}
  }
]

Topology

topology is a list of directed edges, each a {from, to} tuple. Every endpoint must be the name of a defined agent or object. Both endpoints may be atoms or strings (strings are normalized to atoms).

topology: [
  {:researcher, :coder},  # researcher can send to coder
  {:coder, :researcher}   # and back
]

An edge {a, b} permits messages from a to b. For two-way communication, declare both directions explicitly. The topology may be empty.

Validation collects all edge errors and returns them wrapped as {:invalid_topology, errors}. Each entry is either {:unknown_agent, name} (an endpoint that is not a defined agent or object) or {:invalid_edge_format, idx, edge} (an edge that is not a {from, to} tuple of atoms/strings).

System objects

The router always permits messages to the system objects :metrics, :tick, and :gateway, even without an explicit topology edge (@system_objects in lib/genswarms/routing/router.ex). You do not need to declare edges to these targets. See messaging.md for routing details.

Config formats

The file extension determines the parser: .exs (Elixir term), .json, or .yaml/.yml. All formats produce the same validated structure. String keys are atomized and scalar string backend values are converted to atoms during loading.

Elixir (.exs)

The file must evaluate to a configuration map. This is the only format that supports Elixir-native values such as module atoms for object handlers, tuple-form backends, and dynamic expressions.

%{
  name: "example-swarm",
  agents: [
    %{name: :researcher, backend: :local, skills: ["web.md"]},
    %{name: :coder, backend: {:docker, "coder"}, skills: ["code.md"]}
  ],
  topology: [
    {:researcher, :coder},
    {:coder, :researcher}
  ]
}

JSON

Topology edges are two-element arrays. Backends are normally scalar strings; see the JSON/YAML limitation above. Tmux additionally accepts an object such as {"type":"tmux","client":"codex","opts":{"workspace":"/work"}}.

{
  "name": "example-swarm",
  "agents": [
    { "name": "researcher", "backend": "local", "skills": ["web.md"] },
    { "name": "coder", "backend": "bwrap", "skills": ["code.md"] }
  ],
  "topology": [
    ["researcher", "coder"],
    ["coder", "researcher"]
  ]
}

YAML

Same rule as JSON, including the data-only tmux backend object exception.

name: example-swarm
agents:
  - name: researcher
    backend: local
    skills:
      - web.md
  - name: coder
    backend: bwrap
    skills:
      - code.md
topology:
  - [researcher, coder]
  - [coder, researcher]

Per-agent models

Each agent can run on a different model and endpoint. The model is passed to subzeroclaw as SUBZEROCLAW_REQUEST_EXTRA = {"model": …} — there is no SUBZEROCLAW_MODEL env fallback (it is a dead variable); when unset, subzeroclaw uses its own default. The endpoint falls back to SUBZEROCLAW_ENDPOINT and is otherwise auto-detected from the API key. To route through an unhardcoded router, set request_extra with a policy_ir instead of a bare model.

agents: [
  %{name: :researcher, backend: :local, model: "anthropic/claude-sonnet-4", skills: ["web.md"]},
  %{name: :coder, backend: :local, model: "deepseek/deepseek-chat", skills: ["code.md"]}
]

Skill templating

Skill files listed under an agent's skills: are copied into the agent at deploy time, and three template variables are substituted per agent:

Variable Resolves to
{{agent_name}} the agent's name
{{swarm_name}} the swarm name
{{workspace}} the agent's workspace path (empty if unset)

This lets one skill file serve many agents. See skills.md for authoring details and built-in skills.

Full annotated example

%{
  # Required: unique identifier (letter-led, alphanumeric/_/-)
  name: "example-swarm",

  agents: [
    # Local agent with a per-agent model and tool presets
    %{
      name: :researcher,
      backend: :local,
      model: "anthropic/claude-sonnet-4",
      skills: ["web.md", "summarize.md"],
      presets: [:base, :web]
    },

    # Sandboxed bwrap agent: backend keys + domain keys in `config`
    %{
      name: :coder,
      backend: :bwrap,
      skills: ["code.md"],
      config: %{
        # Backend keys (consumed by BwrapBackend)
        workspace: "/tmp/example-swarm/coder",
        memory_limit: "512M",
        presets: [:base, :code],
        # Domain key (available to the agent's logic)
        max_iterations: 25
      }
    }
  ],

  # Optional: deterministic, non-agentic component
  objects: [
    %{
      name: :evaluator,
      handler: MyApp.Objects.Evaluator,
      config: %{parallel: true}
    }
  ],

  # Directed edges; system objects (:metrics, :tick, :gateway)
  # are routable without explicit edges
  topology: [
    {:researcher, :coder},
    {:coder, :evaluator},
    {:evaluator, :researcher}
  ]
}

See also

  • backends.md — backend types and their options
  • containers.md — building NixOS container images for Docker and Apple container agents
  • objects.md — the ObjectHandler behaviour and object patterns
  • skills.md — authoring and deploying agent skill files
  • cli.md — validating and running configs from the command line