Circuit breaker as Worker-level injection
A circuit breaker is a stateful dependency that tracks recent failures for a downstream service and trips to an open state that rejects calls when the failure rate crosses a threshold. This state only protects the service if shared across every Activity execution on the Worker. Injecting a single breaker instance ensures all executions feed the same failure window, and when the downstream service degrades, the breaker opens and Activities fail fast instead of piling up latency.
Activity Dependency Injection pattern overview
The Activity Dependency Injection pattern separates the creation of external dependencies (database connections, API clients, configuration) from Activity business logic by injecting them at Worker startup. This approach keeps Workflow code deterministic, makes Activities testable in isolation, and ensures expensive resources are initialized once per Worker process rather than once per Activity execution.
Worker startup dependency initialization
Dependencies must be initialized once when the Worker process starts. All Activity executions on that Worker share the same instances, which is appropriate for thread-safe resources like connection pools and HTTP clients. Dependencies should be created and validated before calling worker.Run() or its equivalent to ensure the Worker does not start accepting tasks until all dependencies are ready.
Activity Dependency Injection benefits
Resources like connection pools are initialized once and shared across all Activity executions, reducing overhead. Substituting mock implementations in tests requires no changes to Activity or Workflow code. Switching between environments involves changing only the Worker configuration.
Problems solved by Activity Dependency Injection
The pattern addresses four key problems: (1) Reinitializing resources per execution wastes resources and increases latency. (2) Hardcoded dependencies couple business logic to infrastructure, making it difficult to swap implementations across environments. (3) When Activities construct their own dependencies internally, you cannot substitute test doubles without modifying production code. (4) Passing dependencies directly into Workflow code breaks Temporal's determinism guarantees because dependency state can change between replays.
Pitfall: Injecting dependencies into Workflows
This breaks determinism because dependency state can change between the original execution and a replay. The Temporal Java SDK documentation explicitly warns against this.
Pitfall: Using non-thread-safe dependencies
A single mutable object shared across concurrent Activity executions causes race conditions. Use connection pools and ensure all injected objects are safe for concurrent use.
Best practice: Group related Activities on single struct or class
Activities that share the same dependencies belong together. If two groups of Activities have different dependencies, use separate structs or classes for each group.
When Activity Dependency Injection is appropriate
This pattern is a good fit when Activities access external services such as databases, message queues, or third-party APIs. It is appropriate when you want to initialize expensive resources once per Worker process, when you need to test Activity logic without connecting to real services, or when you operate in multiple environments (development, staging, production) that require different dependency configurations.
Circuit breaker libraries by language
Go: sony/gobreaker v2 with generic API. Python: pybreaker with call_async for coroutines. Java: resilience4j, successor to Netflix Hystrix. TypeScript: opossum, wraps a single async action.
Best practice: Define dependencies as interfaces
In Go, Python, and Java, using interfaces (or protocols in Python) for dependencies makes it possible to swap implementations for testing or different environments.
Activity Dependency Injection solution approach
Define Activities as methods on a struct or class that holds dependencies as fields. At Worker startup, instantiate the struct or class with real implementations and register it with the Worker. The Workflow references Activity methods without knowing about the underlying dependencies. During testing, substitute mock or stub implementations into the same Activity struct or class.
Activity Dependency Injection trade-offs
All Activity executions on a given Worker share the same dependency instances. If an Activity requires per-execution isolation (for example, a database transaction scoped to a single Activity), you need to manage that within the Activity method itself. Dependencies must also be thread-safe, since multiple Activity executions may run concurrently on the same Worker.
Best practice: Do not inject dependencies into Workflows
Workflow code must remain deterministic. If a Workflow needs configuration, retrieve it through a Local Activity so the value gets recorded in the Event History.
When Activity Dependency Injection is not necessary
This pattern is not necessary for Activities that are pure functions with no external dependencies, or for Activities that only use Temporal-provided context like heartbeating and logging.
Pitfall: Constructing dependencies inside Activity methods
Creating a new database connection or API client per Activity execution leads to resource exhaustion and increased latency.
Best practice: Keep dependencies thread-safe
Multiple Activity executions run concurrently on the same Worker. Use connection pools rather than single connections, and avoid mutable shared state.
Basic Workflow batch processing pattern
The Basic Workflow (single-tier fan-out) pattern is the most direct form of batch processing where the Workflow fetches or receives record IDs and executes one Activity per record. Activities can be executed sequentially or concurrently using the SDK's async primitives. The limit is 2,000 in-flight Activities per Workflow run (aim for 500). If total event count is likely to exceed 2,000 (hard limit is 51,200), use the Batch Iterator pattern instead. Pros: minimal code and orchestration overhead. Cons: hard cap on concurrent Activities; all-or-nothing failure model; can overwhelm downstream systems.
Temporal Schedules for recurring Workflows
Schedules allow Workflows to be executed on a recurring basis, functioning as a more flexible cron with start, pause, stop, update, and backfill controls. Schedules support start, pause, stop, update, and backfill of scheduled Workflow executions. Configurable Overlap Policies control what happens when the previous run is still running. Full execution history visibility is available in the Temporal UI. Schedules can be created via the UI, CLI, or SDK.
Temporal Workflow limits
Unfinished actions per Workflow: 2,000 max (aim for 500). Includes Activities, Signals, Child Workflows, and cancellation requests. Events per Workflow history: 51,200 events max (aim for a few thousand) or 50 MB total history size; warns at 10,240 events or 10 MB. Signals per Workflow: 10,000. Updates per Workflow: 10 in-flight, 2,000 total. Batch Signalling: 1 batch job per namespace; 50 Workflows per second per batch.
Signal multiple running Workflows with visibility query
The temporal workflow signal command can target multiple Workflows of a given type using a visibility query. The query parameter filters by ExecutionStatus and WorkflowType. Example: temporal workflow signal --name MySignal --input '{"Input": "As-JSON"}' --query 'ExecutionStatus = "Running" AND WorkflowType="YourWorkflow"' --reason "Testing"
Create a Temporal Schedule via CLI
A Temporal Schedule can be created using the temporal CLI with the schedule create command, specifying a schedule-id, workflow-id, task-queue, and workflow-type.
Batch signalling multiple Workflows
The Temporal CLI lets you signal, reset, cancel, or terminate multiple Workflows with a single command using a visibility query. There is 1 running batch job per namespace and a throughput limit of 50 Workflows per second per batch.
Terminate multiple Workflows with visibility query
The temporal workflow terminate command can target multiple Workflows of a given type using a visibility query to terminate all matching Workflows in one operation. Example: temporal workflow terminate --query 'ExecutionStatus = "Running" AND WorkflowType="SomeWorkflowType"' --reason "Terminate Test Workflows"
Batch processing patterns overview and selection
Five batch processing patterns exist for different scenarios. Basic Workflow handles up to a few hundred records with sequential or parallel activities in one Workflow, with no workflow-based rate control. Fan-Out with Child Workflows handles up to approximately 4M records with fixed concurrency (one child per chunk) and no workflow-based rate control. Batch Iterator handles unlimited records with limited activities per page and fixed page rate control. Sliding Window handles unlimited records with bounded window of concurrent children and configurable window rate control. MapReduce Tree handles unlimited records with fully parallel recursive tree and no rate control—maximum speed.
Batch Iterator pattern overview
The Batch Iterator pattern processes a large record set one page at a time. Each Workflow run processes a single page and then calls Continue-as-New with the next offset, producing a chain of short-lived runs that together cover the entire record set without accumulating unbounded event history. This approach is used when the record set is arbitrarily large, you need a durable checkpoint after every page, and sequential page-by-page throughput is acceptable.
Single Workflow run event history and activity limits
A single Workflow run is limited to 50,000 history events with a recommended aim for 2,000 events, and 2,000 in-flight Activities. Processing millions of records in one run is not possible within these bounds.
Batch Iterator implementation steps
1. The Workflow starts with offset=0 and calls fetchPage(offset, pageSize) to retrieve the first page of records. 2. It processes each record in the page by executing the processRecord Activity. 3. After the page is fully processed, it calls continueAsNew with offset + pageSize, passing the updated offset to the next run. 4. The next run begins with a clean history and repeats the same steps for the next page. 5. When fetchPage returns fewer records than pageSize, the Workflow knows it has reached the last page and returns normally.
Batch Iterator pitfall: passing unnecessary state into continueAsNew
All arguments are serialized and stored in history. Pass only the minimal state needed (offset, counters) — not accumulated result lists or large collections that grow with each page.
Batch Iterator pitfall: sequential processing bottleneck
The default implementation processes one record at a time per page. You can fan out Activities concurrently within a page using the SDK's async primitives for higher per-page throughput — note this increases per-page event count accordingly. If record-set-wide throughput matters more than rate limiting, consider Sliding Window or MapReduce Tree patterns.
Batch Iterator avoid accumulating state between pages
Avoid accumulating large local state between pages. continueAsNew does not carry over in-memory state; only the arguments you pass are available in the next run.
Batch Iterator pitfall: forgetting continueAsNew on last page
If you call continueAsNew unconditionally, the Workflow loops forever even when the data source is exhausted. Check whether the returned page is shorter than pageSize before continuing.
Batch Iterator solution: offset and continueAsNew
Each Workflow run fetches one page of records using a persistent offset parameter, processes each record sequentially, and then calls continueAsNew with the incremented offset. The next run picks up exactly where the previous one left off. Because each run processes only a bounded number of records, history stays within limits. The offset acts as a durable checkpoint: if the Workflow is interrupted mid-page, the next run replays only from the start of the current page.
Batch Iterator should include totalProcessed counter
Include totalProcessed (or a similar counter) in the continueAsNew arguments. This lets you observe overall progress via the Workflow input visible in the UI without querying internal state.
Batch Iterator fetchPage must be an Activity
Fetch inside an Activity, not the Workflow. The fetchPage call must be an Activity — not inline Workflow code — so it can interact with external systems and be retried independently.
Batch Iterator page size best practice
Choose a page size that keeps history under 2,000 events. Each page produces roughly 3 × pageSize history events (ActivityTaskScheduled + ActivityTaskStarted + ActivityTaskCompleted). A page size of 500–800 records is a safe target.
Batch Iterator processRecord must be idempotent
Make processRecord idempotent. Activities have at-least-once execution semantics. If a worker crashes after an Activity completes externally but before the completion is recorded in history, Temporal will retry it. Your downstream system must tolerate receiving the same record more than once.
Comparison of modularity, history, outlive parent, and overhead across patterns
Child Workflow has high modularity, independent history (yes), can outlive parent (yes with ABANDON), medium overhead, and separate Workflow ID. Activity has medium modularity, independent history (no), cannot outlive parent, low overhead, and no separate Workflow ID. Separate Workflow + Signals has high modularity, independent history (yes), can outlive parent (yes), high overhead, and separate Workflow ID. Async Lambda has low modularity, independent history (no), cannot outlive parent, very low overhead, and no separate Workflow ID.
Child Workflow best practices: unique IDs, choose policy, handle failures, limit parallelism
Best practices for Child Workflows include: generate unique IDs for children to avoid conflicts; use TERMINATE for tightly coupled children and ABANDON for independent children; catch and handle Child Workflow exceptions appropriately; do not spawn unlimited children, use batch patterns for large datasets; consider Activities first for operations that do not need independent Workflow tracking; configure appropriate Workflow execution timeouts for children; prefer typed stubs over untyped for compile-time safety; track Child Workflow IDs for observability and debugging.
Child Workflow trade-offs: overhead, complexity, history separation, resource usage
Trade-offs of Child Workflows include that each child is a separate Workflow execution with its own history (overhead). There are more moving parts than a single Workflow. Child execution details are not in the parent history but are queryable independently. Async children require explicit synchronization if needed. More Workflow executions mean higher resource usage. Starting a Child Workflow has more overhead than starting an Activity.
Pitfall: treating Child Workflows like Activities
Child Workflows are for orchestration, not for executing external code. If you only need to call an API or run a function, use an Activity instead. Child Workflows are first-class Workflow executions with their own identity and history, making them heavier than Activities for simple operations.
Pitfall: not handling child failures
Child Workflow failures propagate to the parent as a Child Workflow Failure exception (ChildWorkflowFailure in TypeScript and Java, ChildWorkflowError in Python, ChildWorkflowExecutionError in Go), with the underlying cause in its cause field. If you do not catch and handle them, the parent Workflow fails as well.
Child Workflows provide independent identity, history, and lifecycle
Each child executes as an independent Workflow with its own Workflow ID, event history with a 50K event limit, and lifecycle. Unlike Activities which execute code, Child Workflows orchestrate processes and provide Workflow-level semantics including independent tracking, querying, timeouts, and the ability to outlive the parent.
Child workflow must start before parent completes in fire-and-forget pattern
When using fire-and-forget with ABANDON policy, you must wait for the child to start before the parent completes. Without this, the parent could complete before the child is scheduled, and the child would never execute.
Pitfall: spawning unbounded children in a loop
Starting thousands of Child Workflows without batching can overwhelm the Temporal Service and bloat the parent's event history. Instead, use fixed-size batches or a sliding window pattern to control parallelism.
Pitfall: ignoring ParentClosePolicy defaults to TERMINATE
The default ParentClosePolicy is TERMINATE, which kills children when the parent closes. If children must outlive the parent, explicitly set the policy to ABANDON. Failing to do this will cause unexpectedly terminated children.
Pitfall: using synchronous calls when async is needed
Calling a Child Workflow synchronously blocks the parent until the child completes. For long-running children, use the async API (Async.function() in Java, startChild() in TypeScript, start_child_workflow() in Python, or collect Futures without calling .Get() in Go) to avoid stalling the parent.
ParentClosePolicy determines child behavior when parent completes
The ParentClosePolicy enum has three values: TERMINATE (child is terminated when parent closes, for tightly coupled processes), ABANDON (child continues independently, for fire-and-forget or long-running tasks), and REQUEST_CANCEL (child receives cancellation request for graceful cleanup). The default policy is TERMINATE.
Child Workflow vs Activity: when to use each
Use Child Workflows when you need a separate Workflow ID for tracking and querying, the operation may outlive the parent, you need to reuse Workflow logic across multiple parents, you want to execute on different Task Queues, you need independent history and event limits, or you want different timeouts or retry policies at the Workflow level. Use Activities when executing external operations (API calls, database queries), the operation is short-lived, you do not need independent Workflow tracking, the operation is tightly coupled to parent lifecycle, or lower overhead is important. The key distinction is that Activities are for executing code (especially external operations), while Child Workflows are for orchestrating processes that benefit from independent Workflow semantics.
Pitfall: omitting Workflow IDs for Child Workflows
Without explicit Workflow IDs, you lose the ability to deduplicate or look up Child Workflows by a meaningful identifier. Generate deterministic IDs based on business keys to enable proper tracking and deduplication.
Child Workflow benefits: modularity, independent history, outlive parent, concurrent execution
Child Workflows provide modularity by breaking complex logic into reusable units. Each child is a first-class Workflow with its own ID for tracking, its own 50K event history limit, and its own execution timeout configuration. Children can outlive parents with the ABANDON policy. Multiple children can start concurrently. Child failures do not automatically fail the parent. The same Child Workflow can be reused by multiple parents.
Continue-As-New best practice: type safety
In Java, prefer `newContinueAsNewStub()` over untyped `continueAsNew()`. In TypeScript, use the generic `continueAsNew<typeof myWorkflow>()` for type safety.
Continue-As-New best practice: aggressive iteration limits
Set aggressive iteration limits by continuing as new every 100–1000 iterations to prevent history buildup and reduce storage costs. Balance frequency with the overhead of creating new executions.
Catching Continue-As-New exception causes unexpected behavior
In TypeScript and Python, Continue-As-New is implemented by throwing a special exception. Wrapping it in a try-catch or try-except can suppress the transition and cause unexpected behavior. Let the exception propagate unhandled. In Go, return the `ContinueAsNewError` from the Workflow function without wrapping it.
Forgetting to drain Signals before Continue-As-New
Any Signals received but not yet processed are lost when Continue-As-New starts a fresh execution. Drain your Signal channel and carry pending Signals forward as arguments to avoid losing Signals during the transition.
Passing too much state in Continue-As-New
Continue-As-New arguments are serialized into the first event of the new execution. Large payloads slow down startup and increase storage costs. Pass only the minimal state needed to avoid performance and storage overhead.
Continue-As-New trade-offs
Trade-offs to consider with Continue-As-New are that previous execution history is archived separately. You must explicitly pass state as arguments (manual state management). Queries only see the current execution's state. Debugging requires tracing across multiple execution runs. You cannot undo Continue-As-New once triggered.
Caching Run IDs with Continue-As-New
Continue-As-New creates a new Run ID. If external callers cache the old Run ID for Signals or Queries, they will get a 'workflow execution already completed' error. Always use Workflow ID without a Run ID (or an empty Run ID) so the request routes to the currently running execution.
Use built-in continue-as-new suggestion instead of fixed iteration count
Instead of tracking iteration counts manually, use the SDK's built-in suggestion to let Temporal tell you when the history is getting large. Different Workflow paths generate different numbers of events per iteration, so a fixed count may continue too early or too late. Use the SDK's built-in continue-as-new suggestion for accurate detection: `isContinueAsNewSuggested()` in Java, `continueAsNewSuggested` in TypeScript, `is_continue_as_new_suggested()` in Python, and `GetContinueAsNewSuggested()` in Go.
Continue-As-New benefits
Continue-As-New allows you to run Workflows indefinitely without history limits. Fresh history keeps Workflow execution fast. It reduces active storage costs by archiving old event history — more aggressive iteration limits mean more frequent archiving, keeping active storage minimal. The transition is atomic with no gap between old and new execution. You pass state as arguments to the new execution, and the Workflow ID remains the same, maintaining logical continuity for Queries and Signals.