Advanced Plugin API
Payload's plugin system is built around a simple contract: a plugin is a function that receives a config and returns a modified config. That simplicity is intentional and permanent — the basics will never change.
This page covers the advanced plugin API that makes plugins more powerful: execution ordering via order, typed cross-plugin communication via RegisteredPlugins, and the definePlugin helper that ties it all together.
The basics still work
The plain function form is unchanged and will always be supported:
Everything below builds on top of this — none of it is required for simple plugins.
definePlugin — recommended for published plugins
definePlugin replaces the boilerplate of manually attaching slug, order, and options to the function after the fact. Your plugin function receives a single object containing config, a plugins map, and any user-provided options spread directly in:
Import it from payload:
The result of definePlugin is a factory function — call it with your options to get a Plugin:
Execution ordering with order
By default, plugins execute in the order they appear in the plugins array. Setting order lets you declare execution order explicitly, regardless of array position.
Lower order values run first. The default is 0.
Suggested order conventions
Settle on a convention so the ecosystem converges:
Range | Use case |
|---|---|
Negative | Must run before everything — config normalization, polyfills |
| Default — no dependencies on other plugins |
| Depends on collections or fields added by other plugins |
| Must run last — audit, introspection, or final-config plugins |
Cross-plugin communication
Plugins often need to be aware of each other. The pattern for this is:
- A plugin with a
slugexposes itsoptionsobject — the same object passed at call time - Another plugin finds it via the
pluginsmap and mutates those options before the first plugin runs - When the first plugin executes, it sees the mutated options
Since options are resolved before any plugin runs, this works cleanly without re-execution.
The plugins map
Every plugin created with definePlugin receives a plugins map — a slug-keyed object of all plugins in the config. No imports needed:
For registered slugs (see below), the plugins map entries are automatically typed — no cast needed.
RegisteredPlugins — module augmentation for type safety
Plugin packages can register their slug and options type by augmenting the RegisteredPlugins interface. This ships with the package and is activated automatically when the plugin is imported — no code generation required.
Once a plugin package augments RegisteredPlugins, any project that imports it gets typed access via the plugins map:
Full example: two interoperating plugins
Here is a complete example of two decoupled plugins that communicate via the plugins map. The writer plugin (order 1) runs first and injects an item into the reader plugin's options. The reader plugin (order 10) runs second and sees the injected item.
When to use cross-plugin mutation vs. direct options
Use cross-plugin mutation (plugins map + options mutation) when:
- The two plugins are decoupled packages — one doesn't import the other
- The extending plugin is optional — the target plugin should work without it
- You want users to install both plugins independently without wiring them together manually
Use direct options when:
- The relationship is intentional and documented — the user is expected to pass options directly
- The plugins are in the same package and share types already
Checking if a plugin is installed
You can check whether a plugin is present without importing it:
Or via the plugins map inside a definePlugin function:
Was this page helpful?