How to Create and Configure Plugins in Jido: The Complete Developer Guide

You create and configure plugins in Jido by defining a module that use Jido.Plugin with compile-time metadata and callbacks, then declaring them in an agent's plugins: list where per-instance configuration overrides and automatic state key derivation enable composable runtime behavior.

Jido is an open-source Elixir framework for building agent-based systems that treat plugins as pure, composable capabilities. To create and configure plugins in Jido effectively, you must understand the separation between compile-time plugin definitions and their runtime instantiation within agents. This guide explains the complete plugin lifecycle—from code generation to signal routing—based on the actual source implementation in the agentjido/jido repository.

Understanding Jido Plugin Architecture

The Jido plugin system operates through three distinct runtime stages, each managed by specific modules in lib/jido/plugin/.

The Plugin Definition Stage

At compile time, a plugin module invokes use Jido.Plugin which expands the __using__/1 macro defined in lib/jido/plugin.ex. This macro validates options against @plugin_config_schema and generates accessor functions (e.g., name/0, state_key/0, actions/0).

The macro produces two key data structures:

  • Spec (Jido.Plugin.Spec): A per-agent runtime view containing the resolved configuration map, defined in lib/jido/plugin/spec.ex.
  • Manifest (Jido.Plugin.Manifest): Compile-time metadata including capabilities, requirements, and signal routes used by discovery tools, defined in lib/jido/plugin/manifest.ex.

The macro also supplies default implementations for callbacks like mount/2, handle_signal/2, and child_spec/1, which developers can override for custom behavior.

Runtime Instance Resolution

When an agent starts, each plugin declaration transforms into a Jido.Plugin.Instance struct via the logic in lib/jido/plugin/instance.ex. This module normalizes various declaration formats—whether MyModule, {MyModule, %{config: true}}, or {MyModule, as: :alias}—into a standardized struct.

The Instance module derives unique identifiers for each plugin attachment:

  • state_key: Determines where the plugin's state slice lives within the agent's global state map.
  • route_prefix: Prepends to signal types for routing (e.g., "support.chat.send").

If a plugin declares singleton?: true, the Instance module enforces that it cannot be aliased multiple times within the same agent.

Configuration Merging and Validation

Configuration resolution occurs in lib/jido/plugin/config.ex, which merges three sources in order of precedence:

  1. Application environment values retrieved via Application.get_env/3 using the plugin's otp_app.
  2. Per-agent overrides supplied directly in the agent's plugins: declaration.
  3. Default values defined in the plugin's optional config_schema.

The merged configuration validates through Zoi schemas, returning {:ok, config} or {:error, errors} depending on validation results.

Creating a Jido Plugin

Generating the Plugin Scaffold

While manual creation is possible, use the Mix generator to scaffold boilerplate:

mix jido.gen.plugin MyApp.ChatPlugin --signals="chat.*,message.*"

This creates lib/my_app/chat_plugin.ex and corresponding test files via lib/mix/tasks/jido.gen.plugin.ex, pre-populating the module structure with required callbacks.

Defining Plugin Metadata and Schema

Every plugin requires compile-time metadata that declares its capabilities. In lib/jido/plugin.ex, the macro consumes these options to generate manifests and specs:

defmodule MyApp.ChatPlugin do
  use Jido.Plugin,
    name: "chat",
    state_key: :chat,
    actions: [MyApp.Actions.SendMessage, MyApp.Actions.ListHistory],
    schema: Zoi.object(%{
      messages: Zoi.list(Zoi.any()) |> Zoi.default([]),
      model: Zoi.string() |> Zoi.default("gpt-4")
    }),
    signal_patterns: ["chat.*"],
    signal_routes: [
      {"chat.send", MyApp.Actions.SendMessage},
      {"chat.history", MyApp.Actions.ListHistory}
    ],
    config_schema: Zoi.object(%{
      api_key: Zoi.string(),
      model: Zoi.string() |> Zoi.default("gpt-4")
    })

The config_schema option is optional but recommended—it enables automatic validation of configuration maps passed during agent initialization.

Implementing the Mount Callback

The mount/2 callback in lib/jido/plugin.ex initializes plugin-specific state. This callback runs purely without side effects during agent initialization:

  @impl Jido.Plugin
  def mount(_agent, config) do
    # Return initial state merged with defaults from the schema option

    {:ok, %{
      initialized_at: DateTime.utc_now(),
      api_key: config[:api_key],
      model: config[:model]
    }}
  end
end

Defaults defined in the schema option automatically merge with the returned map, creating the final state slice stored under the plugin's state_key.

Configuring Plugins in Agents

Agents declare plugins through the use Jido.Agent macro, which processes the plugins: option during compilation and passes declarations to Jido.Agent.new/1 at runtime.

Basic Plugin Declaration

Include a plugin using its module name to accept default configuration from the application environment:

defmodule MyApp.ChatAgent do
  use Jido.Agent,
    name: "chat_agent",
    plugins: [
      MyApp.ChatPlugin
    ]
end

Per-Agent Configuration Overrides

Override configuration values for a specific agent by passing a configuration map:

plugins: [
  {MyApp.ChatPlugin, %{model: "claude-3", api_key: "sk-override"}}
]

The Jido.Plugin.Config module merges these overrides on top of application environment values, validating the final map against config_schema if defined.

Working with Aliased Instances

Create multiple isolated instances of the same plugin using the as: option, which generates derived state keys and route prefixes:

plugins: [
  {MyApp.ChatPlugin, as: :support, api_key: "support-token"},
  {MyApp.ChatPlugin, as: :sales, api_key: "sales-token"}
]

This produces the following mappings in Jido.Plugin.Instance:

Declaration state_key route_prefix
MyApp.ChatPlugin :chat "chat"
{MyApp.ChatPlugin, as: :support} :chat_support "support.chat"
{MyApp.ChatPlugin, as: :sales} :chat_sales "sales.chat"

Signals prefixed with "support.chat" route to the support instance's actions, while "sales.chat" routes to the sales instance, enabling complete runtime isolation within a single agent.

Runtime Execution and Signal Routing

When Jido.AgentServer starts (defined in lib/jido/agent_server.ex), it processes each plugin instance:

  1. Calls mount/2 to initialize the state slice under the derived state_key.
  2. Starts any child processes defined by the plugin's child_spec/1 callback.
  3. Registers signal bus subscriptions based on signal_patterns or signal_routes.
  4. Routes incoming signals to the appropriate action module according to the manifest.

Dispatch signals to specific plugin instances using the route prefix:


# Routes to the default instance

Jido.AgentServer.cast(agent_pid, %Jido.Signal{
  type: "chat.send",
  payload: %{content: "Hello"}
})

# Routes to the aliased support instance

Jido.AgentServer.cast(agent_pid, %Jido.Signal{
  type: "support.chat.send", 
  payload: %{content: "Help needed"}
})

The handle_signal/2 callback (defaulting to {:ok, nil} in lib/jido/plugin.ex) can be overridden to intercept signals for authentication, logging, or action substitution before routing occurs.

Summary

  • Plugin definition occurs through use Jido.Plugin in lib/jido/plugin.ex, which generates a Spec for runtime data and a Manifest for compile-time metadata.
  • Configuration merges application environment, per-agent overrides, and defaults through Jido.Plugin.Config in lib/jido/plugin/config.ex, with optional Zoi schema validation.
  • Runtime instantiation creates Jido.Plugin.Instance structs in lib/jido/plugin/instance.ex, deriving unique state_key and route_prefix values that enable multiple plugin instances within one agent.
  • Signal routing uses route prefixes to direct messages to specific plugin instances, while the mount/2 callback initializes pure state slices merged into the agent's global state.

Frequently Asked Questions

What is the difference between a Plugin Spec and a Manifest?

According to the source code in lib/jido/plugin/spec.ex and lib/jido/plugin/manifest.ex, the Manifest contains static, compile-time metadata about capabilities, requirements, schedules, and signal routes used for discovery and tooling. The Spec represents the runtime view of an attached plugin within a specific agent, holding the resolved configuration, actual state key, and current signal patterns. While the manifest exists once per plugin module, each agent creates a unique spec during initialization.

How do I override plugin configuration for a specific agent?

Pass a configuration map as the second element of a tuple when declaring the plugin in your agent's use Jido.Agent block: {MyPlugin, %{key: "value"}}. The Jido.Plugin.Config module merges this override on top of the application environment configuration and schema defaults, validating the final result before the agent starts. Invalid configurations return errors during Jido.Agent.new/1 initialization.

Can I use the same plugin module multiple times in one agent?

Yes, by using the as: option to create aliased instances: {MyPlugin, as: :alias_name}. The Jido.Plugin.Instance module in lib/jido/plugin/instance.ex automatically derives unique state_key values (e.g., :myplugin_alias_name) and route prefixes (e.g., "alias_name.myplugin") for each alias. However, if the plugin declares singleton?: true, the Instance module enforces that only one instance—without aliasing—is permitted per agent.

What happens if I don't provide a config_schema?

If you omit the config_schema option in use Jido.Plugin, the configuration system in lib/jido/plugin/config.ex skips schema validation while still merging application environment values with per-agent overrides. Without a schema, no defaults are automatically applied, and the plugin receives whatever configuration map results from the merge process. While functional, lacking a config_schema removes compile-time guarantees and runtime validation for configuration parameters.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →