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
ObjectHandlerbehaviour and object patterns - skills.md — authoring and deploying agent skill files
- cli.md — validating and running configs from the command line