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 inlib/jido/plugin/spec.ex. - Manifest (
Jido.Plugin.Manifest): Compile-time metadata including capabilities, requirements, and signal routes used by discovery tools, defined inlib/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:
- Application environment values retrieved via
Application.get_env/3using the plugin'sotp_app. - Per-agent overrides supplied directly in the agent's
plugins:declaration. - 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:
- Calls
mount/2to initialize the state slice under the derivedstate_key. - Starts any child processes defined by the plugin's
child_spec/1callback. - Registers signal bus subscriptions based on
signal_patternsorsignal_routes. - 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.Plugininlib/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.Configinlib/jido/plugin/config.ex, with optional Zoi schema validation. - Runtime instantiation creates
Jido.Plugin.Instancestructs inlib/jido/plugin/instance.ex, deriving uniquestate_keyandroute_prefixvalues that enable multiple plugin instances within one agent. - Signal routing uses route prefixes to direct messages to specific plugin instances, while the
mount/2callback 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →