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.DynamicCouncil and 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]
end

Any 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
end

The 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):

  1. App-wide default profile: config :council_ex, :default_profile, MyProfile
  2. Council default_profile/1
  3. Member's :profile opt
  4. 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:

ProfileProviderModelNotes
OpenAIBalanced:openaigpt-4oGeneral-purpose default.
OpenAIMini:openaigpt-4o-miniCheap, fast, fine for panel members.
OpenAICreative:openaigpt-4oHigher temperature for divergent writing.
OpenAIDeterministic:openaigpt-4otemperature: 0.0 for judging / tie-breaking.
AnthropicBalanced:anthropicclaude-sonnet-4General-purpose Claude.
GeminiBalanced:geminigemini-2.5-flashNative responseSchema for structured output.
OllamaLocal:ollamallama3.1:8bLocal-LLM smoke tests.
OpenRouterAuto:openrouteropenrouter/autoOpenRouter picks the cheapest healthy model.
OpenRouterClaudeSonnet:openrouteranthropic/claude-sonnet-4Claude via OpenRouter.

Use these directly:

default_profile CouncilEx.Profiles.OpenAIMini

Or as the :profile opt on a single member:

member :judge, MyApp.Members.Critic, profile: CouncilEx.Profiles.OpenAIDeterministic

Dynamic-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.