Never catch Throwable or Error in Workflows and Activities
Workflow and Activity code should only ever catch Exception or a narrower type. Never catch Throwable or Error. The Java SDK uses subclasses of Error as internal control signals that must reach the SDK's own code uncaught. DestroyWorkflowThreadError interrupts a Workflow thread so the Worker can release it back to the pool. UnsupportedVersion is thrown by Workflow.getVersion() when replayed history was produced by code outside the version range and is designed to not be caught by application code.
When to use wrap() on exceptions
Only checked exceptions need wrap(). Any unhandled exception an Activity or Workflow throws — checked or not — is already converted to an ApplicationFailure automatically when it crosses the Activity or Workflow boundary. wrap() exists to satisfy the Java compiler when you want to throw a checked exception from a method that doesn't declare it, not to make the exception propagate. After unwrapping a cause to inspect it, either rethrow the failure you caught or throw a new ApplicationFailure with the original exception set as its cause; there is no wrapper left to reapply.
Failure cause chain structure
An exception thrown from an Activity or Child Workflow arrives at the caller wrapped with context. A failure from an Activity called from a Child Workflow called from a parent Workflow looks like: WorkflowFailedException (thrown to the client) → ChildWorkflowFailure (the child Workflow Execution failed) → ActivityFailure (the Activity Execution failed) → ApplicationFailure (what your code actually threw). Each wrapper adds context: ActivityFailure carries the Activity Type and Activity Id, ChildWorkflowFailure carries the Workflow Type and Workflow Id. Calling getCause() on each layer moves toward what actually failed.
Reading ApplicationFailure details
When reading an ApplicationFailure: (1) Read getOriginalMessage(), not getMessage(). getMessage() returns a decorated string such as 'message=Invalid credit card number, type=ValidationError, nonRetryable=true' meant for logs, not parsing. getOriginalMessage() returns the exact text you threw. (2) Match on getType(), a stable String, not instanceof your original exception class. ApplicationFailure is final and the original exception object doesn't survive serialization. type defaults to the thrown exception's fully qualified class name unless you set it explicitly with ApplicationFailure.newFailure(message, type, ...).
Catch ActivityFailure and ChildWorkflowFailure, not ApplicationFailure directly
When handling Activity and Child Workflow failures, catch ActivityFailure (or ChildWorkflowFailure) around a call, not ApplicationFailure directly, because the Activity or Child Workflow boundary always wraps the underlying failure. Always check for CanceledFailure as the cause before handling anything else, and rethrow it unhandled. This ensures cancellation is not swallowed.
Handle Activity and Child Workflow failures example
Example of correct failure handling: try { return activities.charge(order); } catch (ActivityFailure e) { if (e.getCause() instanceof CanceledFailure) { throw e; // never swallow cancellation } if (e.getCause() instanceof ApplicationFailure appFailure && "PaymentDeclined".equals(appFailure.getType())) { return Result.declined(appFailure.getOriginalMessage()); } throw e; // don't recognize it — propagate }
Wrap checked exceptions example
Example of wrapping a checked exception in an Activity: static class GreetingActivitiesImpl implements GreetingActivities { @Override public String composeGreeting(String greeting, String name) { try { return callExternalService(greeting, name); // declares throws IOException } catch (IOException e) { throw Activity.wrap(e); } } }
Cancellation cleanup in detached Cancellation Scope
If you need cleanup to run after a cancellation — for example, compensating an Activity that already applied its effect — run it in a detached Cancellation Scope, since a normal scope is a child of the one that was just canceled and any call inside it would be canceled immediately. Example: try { activities.longRunningWork(); } catch (CanceledFailure e) { Workflow.newDetachedCancellationScope(() -> activities.compensate()).run(); throw e; // rethrow after cleanup so the Workflow Execution ends Canceled }
Java SDK best practices resources
Best practices documentation covers Testing, Debugging, and Converters and encryption (data handling).
Java SDK Integrations available
Available integrations for Java SDK include: Parseable integration, Spring AI integration, and Spring Boot integration.
Temporal Java SDK Plugin system documentation
The Temporal Java SDK has a Plugin system documented in the plugins-guide that serves as the foundation for integrations.
Java SDK integrations built on Plugin system
Temporal Java SDK integrations are built on the Temporal Java SDK's Plugin system, which developers can also use to build their own integrations.
PHP SDK best practices documentation sections
The PHP SDK best practices documentation covers two main areas: Testing and Debugging. These sections provide guidance on how to implement best practices when developing with the PHP SDK.
Cron Jobs not recommended, use Schedules instead
Temporal recommends using Schedules instead of Cron Jobs for recurring automation. Schedules provide a better developer experience with more configuration options and the ability to update or pause running Schedules.
Retry Policy can work with Workflow timeouts for fine control
A Retry Policy can work in cooperation with Workflow timeouts to provide fine controls to optimize the execution experience.
Use Timer instead of Workflow Timeout for delayed actions
If you need to perform an action inside your Workflow after a specific period of time, use a Timer instead of setting a Workflow Timeout.
Workflow retries use Retry Policy with WorkflowOptions
To enable retries for a Workflow, provide a Retry Policy object via WorkflowOptions for top-level Workflows or via ChildWorkflowOptions for Child Workflows.
Example: Configuring Retry Policy for Workflow in PHP
$workflow = $this->workflowClient->newWorkflowStub(
CronWorkflowInterface::class,
WorkflowOptions::new()->withRetryOptions(
RetryOptions::new()->withInitialInterval(120)
)
);
This example shows how to configure a Retry Policy for a Workflow using RetryOptions with an initial interval of 120 seconds.
Plugin changes can break existing Workflows due to non-determinism
Users may want to keep their Workflows running across deployments of their Worker code. If deployment includes a new version of a plugin, changes to the plugin could break Workflow code that started before the new version was deployed due to non-deterministic behavior from code changes in the plugin. Use patching for substantive changes.
SimplePlugin is the recommended way to build plugins
The recommended way to start building plugins is with a SimplePlugin abstraction. This abstraction will tackle the vast majority of plugins people want to write.
Plugin features: activities, workflows, Nexus operations, data converters, interceptors
A plugin can provide built-in Activities for users to call from Workflows, built-in Workflows callable as Child Workflows or standalone, built-in Nexus Operations, custom Data Converters to alter data formats or provide compression or encryption, Interceptors for middleware functionality, and Context Propagators for passing custom key-value data across boundaries.
Workflow-friendly libraries in plugins must be deterministic
Any code that runs in the Workflow context must be deterministic, meaning it produces the same commands and results when replayed. Do not 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. Code should run quickly since it may be replayed many times during a long Workflow execution.
Put side effects inside Activities or Local Activities in plugins
Side effects should be placed inside Activities or Local Activities in plugin code. This helps Workflows handle being restarted, resumed, or executed in a different process from where it originally began without losing correctness or state consistency.
Python plugin sandbox considerations
In Python, Workflows can run in a sandbox environment to help prevent non-determinism errors. To work for users who use sandboxing, a plugin should specify the Workflow runner it uses. If in sandbox, the plugin may need to add additional passthrough modules.
TypeScript plugin Workflows must be re-exported by users
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.
TypeScript plugin bundler requirements
Users of a plugin which provides Workflow interceptors should always provide the plugin to the bundler if bundling. The plugin should be passed to both bundleWorkflowCode options and Worker.create plugins.
Context propagators from plugins are appended to existing ones
Context propagators registered via a Plugin are appended to any propagators already set by the user or by previous plugins.
Plugin common use cases
Common use cases for plugins include AI Agent SDKs, observability/tracing/logging middleware, adding reliable built-in functionality such as LLM calls, messaging systems, and payments infrastructure, and encryption or compliance middleware.
Plugin decomposition with Activities and Child Workflows
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.
Python plugin example with interceptors
Example of registering interceptors in a Python plugin:
class SomeWorkerInterceptor(temporalio.worker.Interceptor):
pass # Your implementation
class SomeClientInterceptor(temporalio.client.Interceptor):
pass # Your implementation
plugin = SimplePlugin(
"organization.PluginName",
interceptors=[SomeWorkerInterceptor(), SomeClientInterceptor()],
)
Go plugin example with interceptors
Example of registering interceptors in a Go plugin:
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{}},
})
}
Java plugin example with interceptors
Example of registering interceptors in a Java plugin:
public class SomeWorkerInterceptor extends WorkerInterceptorBase {
// Your worker interceptor implementation
}
public class SomeClientInterceptor extends WorkflowClientInterceptorBase {
// Your client interceptor implementation
}
SimplePlugin interceptorPlugin =
SimplePlugin.newBuilder("organization.PluginName")
.addWorkerInterceptors(new SomeWorkerInterceptor())
.addClientInterceptors(new SomeClientInterceptor())
.build();
TypeScript plugin example with interceptors
Example of registering interceptors in a TypeScript plugin:
class MyWorkflowClientInterceptor implements WorkflowClientInterceptor {}
class MyActivityInboundInterceptor implements ActivityInboundCallsInterceptor {}
class MyActivityOutboundInterceptor implements ActivityOutboundCallsInterceptor {}
const workflowInterceptorsPath = '';
const plugin = new SimplePlugin({
name: 'organization.PluginName',
clientInterceptors: {
workflow: [new MyWorkflowClientInterceptor()],
},
workerInterceptors: {
client: {
workflow: [new MyWorkflowClientInterceptor()],
},
workflowModules: [workflowInterceptorsPath],
activity: [
(_: Context) => ({
inbound: new MyActivityInboundInterceptor(),
outbound: new MyActivityOutboundInterceptor(),
}),
],
},
});
DotNet plugin example with interceptors
Example of registering interceptors in a DotNet plugin:
private class SomeClientInterceptor : IClientInterceptor
{
public ClientOutboundInterceptor InterceptClient(
ClientOutboundInterceptor nextInterceptor) =>
throw new NotImplementedException();
}
private class SomeWorkerInterceptor : IWorkerInterceptor
{
public WorkflowInboundInterceptor InterceptWorkflow(
WorkflowInboundInterceptor nextInterceptor) =>
throw new NotImplementedException();
public ActivityInboundInterceptor InterceptActivity(
ActivityInboundInterceptor nextInterceptor) =>
throw new NotImplementedException();
}
SimplePlugin interceptorPlugin = new SimplePlugin(
"organization.PluginName",
new SimplePluginOptions()
{
ClientInterceptors = new List<IClientInterceptor>() { new SomeClientInterceptor() },
WorkerInterceptors = new List<IWorkerInterceptor>() { new SomeWorkerInterceptor() },
});
Ruby plugin example with interceptors
Example of registering interceptors in a Ruby plugin:
class SomeWorkerInterceptor
include Temporalio::Worker::Interceptor::Workflow
def intercept_workflow(next_interceptor)
# Your interceptor implementation
next_interceptor
end
end
class SomeClientInterceptor
include Temporalio::Client::Interceptor
def intercept_client(next_interceptor)
# Your interceptor implementation
next_interceptor
end
end
plugin = Temporalio::SimplePlugin.new(
name: 'organization.PluginName',
client_interceptors: [SomeClientInterceptor.new],
worker_interceptors: [SomeWorkerInterceptor.new]
)
Go plugin example with context propagators
Example of registering context propagators in a Go plugin:
func createContextPropagatorPlugin() (*temporal.SimplePlugin, error) {
return temporal.NewSimplePlugin(temporal.SimplePluginOptions{
Name: "organization.PluginName",
ContextPropagators: []workflow.ContextPropagator{NewMyPropagator()},
})
}
Python plugin sandbox workflow runner configuration
Example of configuring a Python plugin to work with sandboxed workflows:
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 plugin bundler example
Example of using a TypeScript plugin with the bundler:
const bundle = await bundleWorkflowCode({
workflowsPath: require.resolve('./workflows'),
plugins: [plugin],
});
const worker = await Worker.create({
connection,
taskQueue: 'my-task-queue',
workflowBundle: bundle,
plugins: [plugin],
});
Python SDK best practices topics
The Python SDK documentation covers best practices in the following areas: error handling, testing, Python SDK sandbox, debugging, data handling, and sync vs async programming patterns.
Set import notification policy for specific imports
The import notification policy can be set for specific imports by using the sandbox_import_notification_policy context manager from temporalio.workflow.unsafe. Example: with workflow.unsafe.sandbox_import_notification_policy(workflow.SandboxImportNotificationPolicy.SILENT): import pydantic
RAISE_ON_UNINTENTIONAL_PASSTHROUGH policy setting
The RAISE_ON_UNINTENTIONAL_PASSTHROUGH setting is disabled by default and must be explicitly turned on. It will raise an error when a non-passed-through module is imported into the sandbox.
WARN_ON_UNINTENTIONAL_PASSTHROUGH policy setting
The WARN_ON_UNINTENTIONAL_PASSTHROUGH setting is disabled by default and must be explicitly turned on. It emits a warning when a module not included in the passthrough modules list is imported into the sandbox.
WARN_ON_DYNAMIC_IMPORT policy setting
The WARN_ON_DYNAMIC_IMPORT policy setting is enabled by default. A dynamic import occurs when a module is imported after the Workflow is loaded into the sandbox. When this policy is enabled, a warning will be emitted when a module that is not in the passthrough modules list is dynamically imported.
sandbox_import_notification_policy context manager takes precedence
The sandbox_import_notification_policy context manager will always be respected if used in combination with the restrictions customization at worker creation time.
Python sandbox import notification policy overview
The sandbox's import notification policy specifies how the sandbox behaves when it imports modules in a way that may be unintentional. It covers two scenarios: dynamic imports and enforcing module passthrough. Each can be controlled independently.
Restrict datetime.date class example
To restrict the datetime.date class from being used, use dataclasses.replace() on SandboxRestrictions.default and set invalid_module_members to SandboxRestrictions.invalid_module_members_default piped with SandboxMatcher(children={"datetime": SandboxMatcher(use={"date"})}). Pass the modified restrictions to SandboxedWorkflowRunner.
Remove restriction on datetime.date.today() example
To remove a restriction on datetime.date.today(), use dataclasses.replace() on SandboxRestrictions.default and set invalid_module_members to SandboxRestrictions.invalid_module_members_default.with_child_unrestricted("datetime", "date", "today"). Then pass the modified restrictions to SandboxedWorkflowRunner.
Customize SandboxedWorkflowRunner restrictions
When creating the Worker, the workflow_runner defaults to SandboxedWorkflowRunner(). The SandboxedWorkflowRunner init accepts a restrictions keyword argument that defines a set of restrictions to apply to the sandbox. The SandboxRestrictions dataclass contains four fields, with three having notable values: passthrough_modules, invalid_module_members, and import_notification_policy.
Invalid module members configuration
invalid_module_members includes modules and members that cannot be accessed in the sandbox. Checks are compared against the fully qualified path to the item. Restrictions can be removed using with_child_unrestricted() or added by piping SandboxMatcher instances together with the pipe operator (|).
Pass through modules at worker creation time
Passthrough modules can be configured at Worker creation time by customizing the runner's restrictions. Use SandboxedWorkflowRunner(restrictions=SandboxRestrictions.default.with_passthrough_modules("module_name")) when creating the Worker.
Pass through modules at import time in workflow
One way to pass through a module is at import time in the Workflow file using the imports_passed_through context manager from temporalio.workflow.unsafe. Example: with workflow.unsafe.imports_passed_through(): import pydantic
Passthrough modules benefit and requirements
By default, the sandbox completely reloads non-standard-library and non-Temporal modules for every Workflow run. Passing through a module means it will not be reloaded every time the Workflow runs, improving performance because importing a module can be time-consuming. Only known-side-effect-free third-party modules should be passed through—those that don't have unintended consequences when imported and used multiple times and are known to be deterministic.
Skip sandboxing for a worker
To skip a sandbox environment for a Worker, set the workflow_runner keyword argument of the Worker init to UnsandboxedWorkflowRunner(). This disables sandboxing for all workflows running on that worker.
Skip sandboxing for an entire workflow
To skip a sandbox environment for a Workflow, set the sandboxed argument in the @workflow.defn decorator to false. The entire Workflow will run without sandbox restrictions.
Skip sandboxing for a code block
To skip a sandbox environment for a specific block of code in a Workflow, use the sandbox_unrestricted() context manager from temporalio.workflow.unsafe. The code within the context manager will run without sandbox restrictions.
Python sandbox passthrough modules default behavior
If a module is imported by the Workflow file, a known set which includes all of Python's standard library and Temporal modules is passed through from outside the sandbox. These modules are expected to be free of side effects and have their non-deterministic aspects restricted.
Python sandbox components
The Sandbox environment consists of two main components: global state isolation and restrictions. Global state isolation uses exec to compile and evaluate statements. Upon the start of a Workflow, the file in which the Workflow is defined is imported into a newly created sandbox. Restrictions prevent known non-deterministic library calls by using proxy objects on modules wrapped around a custom importer set in the sandbox.
Python sandbox is not completely isolated
The Temporal Workflow Sandbox for Python is not completely isolated, and some libraries can internally mutate state, which can result in breaking determinism.
Set import notification policy at worker creation time
Import notification policy can be set at worker creation time by customizing the runner's restrictions. Use SandboxedWorkflowRunner(restrictions=SandboxRestrictions.default.with_import_notification_policy(...)) when creating the Worker. Multiple policies can be combined using the pipe operator (|).
Python SDK sandbox environment purpose
The Temporal Python SDK offers a sandbox environment to run Workflow code to help prevent non-determinism errors in applications. If a Workflow Execution performs a non-deterministic event, an exception is thrown, which results in failing the Task Worker. The Workflow will not progress until the code is fixed.