Plugins guide | Temporal Platform Documentation
A Plugin is an abstraction that allows you to customize any aspect of your Temporal Worker setup, including registering Workflow and Activity definitions, modifying worker and client options, and more. Using plugins, you can build reusable open-source libraries or build add-ons for engineers at your company.
This guide will teach you how to create plugins and give platform engineers general guidance on using and managing Temporal's primitives.
Here are some common use cases for plugins:
- AI Agent SDKs
- Observability, tracing, or logging middleware
- Adding reliable built-in functionality such as LLM calls, messaging systems, and payments infrastructure
- Encryption or compliance middleware
How to build a Plugin
The recommended way to start building plugins is with a SimplePlugin. This abstraction will tackle the vast majority
of plugins people want to write.
For advanced use cases, you can extend the methods in lower-level classes that Simple Plugin is based on without re-implementing what you’ve done. See the Advanced Topics section for more information.
Example Plugins
If you prefer to learn by getting hands-on with code, check out some existing plugins.
- Temporal's Python SDK ships with an OpenAI Agents SDK plugin
- Temporal client and Worker plugin for Pydantic AI
- Temporal's TypeScript SDK ships with an OpenTelemetry Plugin
What you can provide to users in a plugin
There are a number of features you can give your users with a plugin. Here's a short list of some of the things you can do.
- Built-in Activities
- Workflow-friendly libraries
- Built-in Workflows
- Built-in Nexus Operations
- Custom Data Converters
- Interceptors
Built-in Activity
You can provide built-in Activities in a Plugin for users to call from their Workflows. Check out the Activities page for more details on how they work.
Refer to the best practices for creating Activities when you are making Activity plugins.
Timeouts and retry policies
Temporal's Activity retry mechanism gives applications the benefits of Durable Execution. See the Activity retry policy explanation for more details.
features/snippets/plugins/plugins.go
func SomeActivity(ctx context.Context) error {
// Activity implementation
return nil
}
func createActivityPlugin() (*temporal.SimplePlugin, error) {
return temporal.NewSimplePlugin(temporal.SimplePluginOptions{
Name: "organization.PluginName",
RunContextBefore: func(ctx context.Context, options temporal.SimplePluginRunContextBeforeOptions) error {
options.Registry.RegisterActivityWithOptions(
SomeActivity,
activity.RegisterOptions{Name: "SomeActivity"},
)
return nil
},
})
}
Workflow-friendly libraries
You can provide a library for use within a Workflow if you'd like to abstract away some Temporal-specific details for your users. Your library will call elements you include in your Plugin such as Activities, Child Workflows, Signals, Updates, Queries, Nexus Operations, Interceptors, Data Converters, and any other code as long as it follows these requirements:
- Any code that runs in the Workflow context must be deterministic, meaning it produces the same commands and results when replayed. For example, don't call system time APIs, generate random values, or perform direct network and file I/O from Workflow-context code; move that work to Activities or Nexus Operations.
- See observability to avoid duplicating observation side effects when Workflows replay.
- Put other side effects inside of Activities or Local Activities. This helps your Workflow handle being restarted, resumed, or executed in a different process from where it originally began without losing correctness or state consistency.
- See testing your Plugin to write tests that check for issues with side effects.
- It should run quickly since it may be replayed many times during a long Workflow execution. More expensive code should go in Activities or Nexus Operations.
A Plugin should allow a user to decompose their Workflows into Activities, as well as Child Workflows and Nexus Calls when needed. This gives users granular control through retries and timeouts, debuggability through the Temporal UI, operability with resets, pauses, and cancels, memoization for efficiency and resumability, and scalability using task queues and Workers.
Users use Workflows for:
- Orchestration and decision-making
- Interactivity via message-passing
- Tracing and observability
Making changes to your library
Your users may want to keep their Workflows running across deployments of their Worker code. If their deployment includes a new version of your Plugin, changes to your Plugin could break Workflow code that started before the new version was deployed. This can be due to non-deterministic behavior from code changes in your Plugin.
See testing to see how to test for this. And, if you make substantive changes, you need to use patching.
Example of a Workflow library that uses a Plugin in Python
Built-in Workflows
You can provide a built-in Workflow in a SimplePlugin. It’s callable as a Child Workflow or standalone. When you want
to provide a piece of functionality that's more complex than an Activity, you can:
- Use a Workflow Library that runs directly in the end user’s Workflow
- Add a Child Workflow
Consider adding a Child Workflow when one or more of these conditions applies:
- That child should outlive the parent.
- The Workflow Event History would otherwise not scale in parent Workflows.
- When you want a separate Workflow ID for the child so that it can be operated independently of the parent's state (canceled, terminated, paused).
Any Workflow can be run as a standalone Workflow or as a Child Workflow, so registering a Child Workflow in a
SimplePlugin is the same as registering any Workflow.
features/snippets/plugins/plugins.go
func HelloWorkflow(ctx workflow.Context, name string) (string, error) {
return "Hello, " + name + "!", nil
}
func createWorkflowPlugin() (*temporal.SimplePlugin, error) {
return temporal.NewSimplePlugin(temporal.SimplePluginOptions{
Name: "organization.PluginName",
RunContextBefore: func(ctx context.Context, options temporal.SimplePluginRunContextBeforeOptions) error {
options.Registry.RegisterWorkflowWithOptions(
HelloWorkflow,
workflow.RegisterOptions{Name: "HelloWorkflow"},
)
return nil
},
})
}
Built-in Nexus Operations
Nexus calls are used from Workflows similar to Activities and you can check out some common Nexus Use Cases. Like Activities, Nexus Call arguments and return values must be serializable.
features/snippets/plugins/plugins.go
type WeatherInput struct {
City string `json:"city"`
}
type Weather struct { City string `json:"city"` TemperatureRange string `json:"temperatureRange"` Conditions string
`json:"conditions"` }
var WeatherService = nexus.NewService("weather-service")
var GetWeatherOperation = nexus.NewSyncOperation( "get-weather", func(ctx context.Context, input WeatherInput, options
nexus.StartOperationOptions) (Weather, error) { return Weather{ City: input.City, TemperatureRange: "14-20C",
Conditions: "Sunny with wind.", }, nil }, )
func createNexusPlugin() (\*temporal.SimplePlugin, error) { return
temporal.NewSimplePlugin(temporal.SimplePluginOptions{ Name: "organization.PluginName", RunContextBefore: func(ctx
context.Context, options temporal.SimplePluginRunContextBeforeOptions) error {
options.Registry.RegisterNexusService(WeatherService) return nil }, }) }
Custom Data Converters
A custom Data Converter can alter data formats or provide compression or encryption.
Note that you can use an existing Data Converter such as, in Python, PydanticPayloadConverter in your Plugin.
features/snippets/plugins/plugins.go
func createConverterPlugin() (*temporal.SimplePlugin, error) {
customConverter := converter.GetDefaultDataConverter() // Or your custom converter
return temporal.NewSimplePlugin(temporal.SimplePluginOptions{
Name: "organization.PluginName",
DataConverter: customConverter,
})
}
Interceptors
Interceptors are middleware that can run before and after various calls such as Activities, Workflows, and Signals. You can learn more about interceptors for the details of implementing them. They're used to:
- Create side effects such as logging and tracing.
- Modify arguments, such as adding headers for authorization or tracing propagation.
features/snippets/plugins/plugins.go
type SomeWorkerInterceptor struct {
interceptor.WorkerInterceptorBase
}
type SomeClientInterceptor struct {
interceptor.ClientInterceptorBase
}
func createInterceptorPlugin() (*temporal.SimplePlugin, error) {
return temporal.NewSimplePlugin(temporal.SimplePluginOptions{
Name: "organization.PluginName",
WorkerInterceptors: []interceptor.WorkerInterceptor{&SomeWorkerInterceptor{}},
ClientInterceptors: []interceptor.ClientInterceptor{&SomeClientInterceptor{}},
})
}
Special considerations for different languages
Each of the SDKs has nuances you should be aware of so you can account for it in your code.
Python
You can choose to run your Workflows in a sandbox in Python. This lets you run Workflow code in a sandbox environment to help prevent non-determinism errors in your application. To work for users who use sandboxing, your Plugin should specify the Workflow runner that it uses.
features/snippets/plugins/plugins.py
def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner:
if not runner:
raise ValueError("No WorkflowRunner provided to the plugin.")
# If in sandbox, add additional passthrough
if isinstance(runner, SandboxedWorkflowRunner):
return dataclasses.replace(
runner,
restrictions=runner.restrictions.with_passthrough_modules("module"),
)
return runner
plugin = SimplePlugin("organization.PluginName", workflow_runner=workflow_runner)
TypeScript
TypeScript bundles cannot provide built-in Workflows because the TypeScript SDK bundles all Workflow code from a single module. Plugin users must import and re-export any Plugin-provided Workflows from their own Workflow module so the bundle includes them.
Users of a plugin which provides Workflow interceptors should always provide the plugin to the bundler if bundling. If you aren't aware of the exact function of the plugin, you can always provide it, as it won't have any adverse effects.
features/snippets/plugins/plugins.ts
const bundle = await bundleWorkflowCode({
workflowsPath: require.resolve('./workflows'),
plugins: [plugin],
});
const worker = await Worker.create({
connection,
taskQueue: 'my-task-queue',
workflowBundle: bundle,
plugins: [plugin],
});
Testing your Plugin
To test your Plugin, you'll write a normal Temporal Workflow tests, having included the plugin in your Client.
Two special concerns are versioning tests, for when you're making changes to your plugin, and testing unwanted side effects.
Versioning tests
When you make changes to your plugin after it has already shipped to users, we recommend that you set up replay testing on each important change to make sure that you’re not causing non-determinism errors for your users.
Side effects tests
Your Plugin should cater to Workflows resuming in different processes than the ones they started on and then replaying from the beginning, which can happen, for example, after an intermittent failure.
You can ensure you're not depending on local side effects by turning Workflow caching off, which will mean that the Workflow replays from the top each time it progresses:
Check for duplicate side effects or other types of failures.
It's harder to test against side effects to global variables, so this practice is best avoided entirely.