Data-only council representation. Build at runtime, edit, persist to storage, restore, run — all without defining an Elixir module.
Designed for UI-driven workflow builders (React Flow, etc.) where the council shape is user data, not code.
Builder pattern
alias CouncilEx.DynamicCouncil
DynamicCouncil.new("seo-council-1", name: "SEO audit")
|> DynamicCouncil.set_default_profile("openai_mini")
|> DynamicCouncil.add_member(%{id: "seo", system_prompt: "Audit SEO."})
|> DynamicCouncil.add_member(%{id: "content", system_prompt: "Audit content."})
|> DynamicCouncil.add_round(:independent_analysis)
|> DynamicCouncil.set_chair(%{id: "synth", system_prompt: "Synthesize."})Persistence
{:ok, json} = DynamicCouncil.to_json(council)
{:ok, council} = DynamicCouncil.from_json(json)Validation
case DynamicCouncil.validate(council) do
:ok -> ...
{:error, errors} -> # JSON-friendly error list
endRun
{:ok, result} = CouncilEx.run(council, %{question: "..."})
Summary
Functions
Append a tool to the council. Council-level tools are merged into every member's tool list at runtime, exposing a shared toolset (e.g. a retrieval tool every member can call).
Append a member. Accepts a %DynamicMember{}, a map, or a keyword list.
Append a round. Accepts a %DynamicRound{}, a map, a string, or an atom.
Latest known schema version.
Decode a JSON string into a %DynamicCouncil{}.
Same as from_json/1 but raises on decoder failure.
Build a %DynamicCouncil{} from a plain map (e.g. JSON-decoded).
Build a new empty council.
Replace the member with the given id, or add it if not present.
Remove a member by id.
Set the chair (or nil to clear). Accepts struct, map, or keyword list.
Replace the council-level tool list outright.
Set the default profile (registered profile name).
Replace the metadata map.
Set the router (registered router name) or nil to clear.
Convert a council to a {nodes, edges} graph suitable for rendering in
React Flow (or any node-graph UI).
Serialize council to a JSON string via Jason.
Same as to_json/2 but raises on encoder failure.
JSON-friendly map representation (string keys).
Lower a %DynamicCouncil{} to a %CouncilEx.Spec{} ready for the runner.
Validate the council. Returns :ok or {:error, [%{path, message, code}]}
with JSON-friendly error structures suitable for UI display.
Same as validate/1 but raises on failure.
Types
@type t() :: %CouncilEx.DynamicCouncil{ chair: CouncilEx.DynamicMember.t() | nil, default_profile: String.t() | nil, id: String.t(), members: [CouncilEx.DynamicMember.t()], metadata: map(), name: String.t() | nil, rounds: [CouncilEx.DynamicRound.t()], router: String.t() | nil, tools: [CouncilEx.DynamicMember.tool_ref()], version: pos_integer() }
Functions
@spec add_council_tool(t(), CouncilEx.DynamicMember.tool_ref()) :: t()
Append a tool to the council. Council-level tools are merged into every member's tool list at runtime, exposing a shared toolset (e.g. a retrieval tool every member can call).
Accepts the same tool refs as member-level tools — a registered tool name (string) or a tool module (atom).
DynamicCouncil.add_council_tool(council, "search_docs")
DynamicCouncil.add_council_tool(council, MyApp.Tools.SearchDocs)
@spec add_member(t(), CouncilEx.DynamicMember.t() | map() | keyword()) :: t()
Append a member. Accepts a %DynamicMember{}, a map, or a keyword list.
Append a round. Accepts a %DynamicRound{}, a map, a string, or an atom.
@spec current_version() :: pos_integer()
Latest known schema version.
Decode a JSON string into a %DynamicCouncil{}.
Same as from_json/1 but raises on decoder failure.
Build a %DynamicCouncil{} from a plain map (e.g. JSON-decoded).
Build a new empty council.
DynamicCouncil.new("council-uuid-...", name: "My SEO Council")
@spec put_member(t(), CouncilEx.DynamicMember.t() | map() | keyword()) :: t()
Replace the member with the given id, or add it if not present.
Remove a member by id.
@spec set_chair(t(), CouncilEx.DynamicMember.t() | map() | keyword() | nil) :: t()
Set the chair (or nil to clear). Accepts struct, map, or keyword list.
@spec set_council_tools(t(), [CouncilEx.DynamicMember.tool_ref()]) :: t()
Replace the council-level tool list outright.
Set the default profile (registered profile name).
Replace the metadata map.
Set the router (registered router name) or nil to clear.
Convert a council to a {nodes, edges} graph suitable for rendering in
React Flow (or any node-graph UI).
Nodes:
- one per member (
type: "member") - one per round (
type: "round") — execution sequence - one for the chair if present (
type: "chair")
Edges:
- council root → each round (sequential)
- each round → each member that participates (currently every member; router-aware filtering deferred until router data is part of the dynamic spec)
- last round → chair (if present)
Returned shapes are plain maps with string keys — JSON-friendly.
Serialize council to a JSON string via Jason.
Same as to_json/2 but raises on encoder failure.
JSON-friendly map representation (string keys).
@spec to_spec(t()) :: CouncilEx.Spec.t()
Lower a %DynamicCouncil{} to a %CouncilEx.Spec{} ready for the runner.
Resolves profile / tool / schema / router refs via the registry. The
returned spec has members containing %DynamicMember{} structs in the
module slot — RoundExec dispatches on them via CouncilEx.MemberSpec.
Validate the council. Returns :ok or {:error, [%{path, message, code}]}
with JSON-friendly error structures suitable for UI display.
@spec validate!(t()) :: :ok
Same as validate/1 but raises on failure.