A Profile bundles the capability stack (provider, model,
temperature, max_tokens, tools, retry) separately from the Member, which
captures identity (role, system prompt, output schema). Same Member,
different Profile = same brain, different model.
Profiles are an ergonomic layer on top of inline provider: /
model: / tools: opts. You can ship a council without ever defining a
Profile module; reach for them once duplication starts hurting.
When to use a Profile
- Multiple members share the same provider + model + tool stack.
- You want to swap "all members onto Anthropic" by changing one line.
- You want a per-member override surface (
profile: MyProfile) that's legible at the council declaration. - You're using
CouncilEx.DynamicCounciland want the UI to pick from a curated set of capability bundles by name.
Defining a profile
defmodule MyApp.Profiles.WebHeavy do
use CouncilEx.Profile
provider :openai
model "gpt-4o"
temperature 0.3
tools [MyApp.Tools.WebFetch]
endAny opt accepted by a member macro is valid in a Profile: provider,
model, temperature, max_tokens, stream, retry, tools,
parallel_tools, parallel_tools_strategy, tool_concurrency_factor,
tool_timeout_ms, tool_choice.
Applying a profile
defmodule MyApp.PaperCouncil do
use CouncilEx
default_profile MyApp.Profiles.WebHeavy
member :alice, MyApp.Members.Researcher # uses default
member :bob, MyApp.Members.Researcher, profile: MyApp.Profiles.LocalCheap # override
member :judge, MyApp.Members.Critic, temperature: 0.0 # default + tweak
endThe two-arg member :id, MyMember shorthand exists precisely for the
default-profile-covers-everything case.
Resolution order
When the runner builds a member's effective opts, it merges in this order (later wins):
- App-wide default profile:
config :council_ex, :default_profile, MyProfile - Council
default_profile/1 - Member's
:profileopt - Inline opts on the member line (e.g.
temperature: 0.0)
So the member line is always the last word; profiles are defaults you can override per-member without rewriting them.
Prebaked profiles
Nine profiles ship in CouncilEx.Profiles.* covering the common picks:
| Profile | Provider | Model | Notes |
|---|---|---|---|
OpenAIBalanced | :openai | gpt-4o | General-purpose default. |
OpenAIMini | :openai | gpt-4o-mini | Cheap, fast, fine for panel members. |
OpenAICreative | :openai | gpt-4o | Higher temperature for divergent writing. |
OpenAIDeterministic | :openai | gpt-4o | temperature: 0.0 for judging / tie-breaking. |
AnthropicBalanced | :anthropic | claude-sonnet-4 | General-purpose Claude. |
GeminiBalanced | :gemini | gemini-2.5-flash | Native responseSchema for structured output. |
OllamaLocal | :ollama | llama3.1:8b | Local-LLM smoke tests. |
OpenRouterAuto | :openrouter | openrouter/auto | OpenRouter picks the cheapest healthy model. |
OpenRouterClaudeSonnet | :openrouter | anthropic/claude-sonnet-4 | Claude via OpenRouter. |
Use these directly:
default_profile CouncilEx.Profiles.OpenAIMiniOr as the :profile opt on a single member:
member :judge, MyApp.Members.Critic, profile: CouncilEx.Profiles.OpenAIDeterministicDynamic-form profiles
CouncilEx.DynamicCouncil references profiles by registered name
string, not module atom. Register at runtime or via config:
# Config
config :council_ex, :registry,
profiles: %{
"openai_mini" => CouncilEx.Profiles.OpenAIMini,
"openai_balanced" => CouncilEx.Profiles.OpenAIBalanced
}
# Or runtime
:ok = CouncilEx.Registry.register_profile("my_custom", MyApp.Profiles.Custom)
# Then reference by name in the dynamic council
DynamicCouncil.new("c1") |> DynamicCouncil.set_default_profile("openai_mini")Dynamic members also accept profile_overrides: %{temperature: 0.9} to
patch the resolved profile on a per-member basis. See
Council forms → Dynamic.
Example
examples/profile_example.exs demonstrates a council mixing
OpenAIMini (panel) and OpenAIBalanced (chair), plus per-member
overrides.