An org is the first segment of an agent's ID: acme for acme/triage and acme/@maria/assistant. Give each org its own settings with a Wardrik.Orgs module. It reads them from wherever your app keeps orgs. Here, a map stands in for your billing plans.

See it in action: Spend you can cap

Setup

In a fresh IEx session:

Mix.install([{:wardrik, "~> 0.1.0-rc.1"}])

defmodule MyApp.Orgs do
  @behaviour Wardrik.Orgs

  @plans %{"acme" => %{model: "anthropic:claude-haiku-4-5", usd: 20.00}}

  @impl true
  def settings(org, _config) do
    case Map.fetch(@plans, org) do
      {:ok, plan} ->
        {:ok,
         %Wardrik.OrgSettings{
           policy: [deny: ["shell/*"]],
           policies: %{"careful" => [ask: ["github/*"]]},
           defaults: %{"model" => plan.model},
           ceilings: %{"plan" => [usd: plan.usd, per: :month]}
         }}

      :error ->
        {:error, Wardrik.Error.new(:not_found, "There's no org #{org} here.")}
    end
  end
end

{:ok, _} = Wardrik.start_link(name: MyApp.AgentRuntime, orgs: {MyApp.Orgs, []})

scope =
  Wardrik.as(MyApp.AgentRuntime, "acme/@maria",
    roles: [:owner],
    resources: ["acme/@maria/*", "acme/ceiling"]
  )
  • policy - every agent of the org is held to it, after the deployment's and before its own. The strictest wins. A list is held to in turn.
  • policies - policies the org's definitions may name, beside the deployment's. A name the deployment already uses is refused.
  • defaults - what the org's definitions get where they say nothing, over the deployment's.
  • ceilings - see Ceilings.

An acme definition can leave out model, and name careful:

Wardrik.Agent.define(scope, "acme/@maria/assistant", policy: "careful")
#=> {:ok, %Wardrik.Agent{id: "acme/@maria/assistant", status: :idle, ...}}

Wardrik asks for an org's settings each time one of its agents starts, wakes or is defined. Settings that don't read keep the agent from starting, and say why.

To bring the org's awake agents to settings that changed:

Wardrik.refresh(MyApp.AgentRuntime, org: "acme")
#=> []

It returns a line for each agent it couldn't refresh. Sleeping agents take the settings as they wake.

Unknown orgs

settings/2 above returns an error for an org that doesn't exist. Its agents aren't defined or started:

sam = Wardrik.as(MyApp.AgentRuntime, "globex/@sam", roles: [:owner], resources: ["globex/@sam/*"])

Wardrik.Agent.define(sam, "globex/@sam/assistant", model: "anthropic:claude-haiku-4-5")
#=> {:error, %Wardrik.Error{type: :not_found, message: "There's no org globex here."}}

Nothing is kept for it either: no secret, server or connection of its owners, and no token whose resources name it, as globex/agent/*. A pattern that leaves the org open, as */agent/*, reaches only the orgs that exist.

Servers per org

An MCP server in Wardrik's :mcp option serves every org, unless its orgs: lists the ones whose agents reach it, as [stripe: [orgs: ["payments"], transport: ...]]. See MCP servers.

Ceilings

A ceiling is a budget every agent of the org spends from together, its users' agents included. It takes what a budget takes, except per: :turn.

Once the org's agents have spent its limit, the next model call any of them makes does what on_exhausted says:

  • :ask - the default. Waits for someone allowed ceiling.approve on acme/ceiling, as an owner is. Approving allows the ceiling's grant more to the whole org for that period:

    acme's agents have spent $20.04 of $20.00 this month. Approve to allow $20.00 more.

  • :deny - refuses the call.

  • :continue - lets it run, so work always carries on.

Reaching a ceiling's limit writes wardrik.ceiling.reached to the agent's log. A ceiling in dollars refuses a model with no price, unless it continues.

Wardrik.Ceilings.list/2 returns what an org has spent against each ceiling. It needs ceiling.read on acme/ceiling, as a reader has. Over HTTP it's GET /v1/ceilings?owner=acme.

Wardrik.Ceilings.list(scope, owner: "acme")
#=> {:ok,
#=>  [
#=>    %Wardrik.Spend{
#=>      ceiling: "plan",
#=>      per: :month,
#=>      on_exhausted: :ask,
#=>      limit: %Wardrik.Amount{usd: 20.0, tokens: nil},
#=>      spent: %Wardrik.Amount{usd: 0, tokens: 0},
#=>      starts_over: ~D[2026-11-01],
#=>      ...
#=>    }
#=>  ]}