Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/best-practices/multi-tenant-patterns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ This pattern works well when you have many tenants with different service tiers

<RelatedReadContainer>
<RelatedReadItem path="/develop/task-queue-priority-fairness#task-queue-fairness" text="Task Queue Fairness Reference" archetype="feature-guide" />
<RelatedReadItem path="/design-patterns/fairness" text="Fairness pattern" />
<RelatedReadItem path="/design-patterns/priority-task-queues" text="Priority Task Queues pattern" />
</RelatedReadContainer>

### 3. Shared Workflow Task Queues, separate Activity Task Queues
Expand Down
21 changes: 19 additions & 2 deletions docs/design-patterns/activity-dependency-injection.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ id: activity-dependency-injection
title: "Activity Dependency Injection"
sidebar_label: "Activity Dependency Injection"
description: "Injects external dependencies into Activities at Worker startup, keeping Workflow code deterministic and Activities testable."
tags:
- Design Patterns
- Workers
---

import Tabs from '@theme/Tabs';
Expand Down Expand Up @@ -740,9 +743,17 @@ Because the breaker counts failures, size the Activity retry policy accordingly.

## When to use

This pattern is a good fit when your 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.
**Good fit:**

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.
- Activities access external services such as databases, message queues, or third-party APIs
- You want to initialize expensive resources once per Worker process
- You need to test Activity logic without connecting to real services
- You operate in multiple environments (development, staging, production) that require different dependency configurations

**Poor fit:**

- Activities are pure functions with no external dependencies
- Activities only use Temporal-provided context, such as heartbeating and logging

## Benefits and trade-offs

Expand Down Expand Up @@ -773,6 +784,12 @@ The trade-off is that all Activity executions on a given Worker share the same d
- **[Entity Workflow](/design-patterns/entity-workflow)**: Long-lived Workflows that manage stateful entities, often using Activities with injected dependencies.
- **[Worker-Specific Task Queues](/design-patterns/worker-specific-taskqueue)**: Routing Activities to specific Workers, which can have different injected dependencies.

### References

- [Activities (Go)](/develop/go/activities/basics): Struct-based Activities sharing a DB pool, client connection, or other process-level resources.
- [Activities (TypeScript)](/develop/typescript/activities/basics): The factory-function pattern for sharing dependencies between Activities.
- [Worker deployment and performance](/best-practices/worker): A reference-app example of registering an Activity struct with injected configuration.

### Sample code

### Go
Expand Down
3 changes: 3 additions & 0 deletions docs/design-patterns/approval.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ id: approval
title: "Approval Pattern"
sidebar_label: "Approval"
description: "Human-in-the-loop Workflows that block until external approval decisions are made. Uses Signals to capture approval data with metadata."
tags:
- Design Patterns
- Signals
---

import Tabs from '@theme/Tabs';
Expand Down
3 changes: 3 additions & 0 deletions docs/design-patterns/batch-iterator.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ id: batch-iterator
title: "Batch Iterator"
sidebar_label: "Batch Iterator"
description: "Pages through unbounded datasets using Continue-As-New to prevent history overflow while maintaining exactly-once processing guarantees."
tags:
- Design Patterns
- Workflows
---

import Tabs from '@theme/Tabs';
Expand Down
6 changes: 4 additions & 2 deletions docs/design-patterns/batch-processing-patterns.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ id: batch-processing-patterns
title: "Batch Processing Patterns"
sidebar_label: "Batch Processing Patterns"
description: "Compare Fan-Out, Batch Iterator, Sliding Window, and MapReduce Tree patterns for processing large record sets reliably at scale."
tags:
- Design Patterns
---

import PatternCards from '@site/src/components/PatternCards';
Expand All @@ -14,7 +16,7 @@ These patterns process large volumes of records reliably, at scale, and without
| Pattern | Record set size | Parallelism model | Workflow-based rate control |
|---|---|---|---|
| [Basic Workflow](#basic-workflow-single-tier-fan-out) | Small (up to a few hundred records) | Sequential or parallel activities in one Workflow | No |
| [Fan-Out with Child Workflows](/design-patterns/fanout-child-workflows) | Up to ~4M records | Fixed concurrency (one child per chunk) | No |
| [Fan-Out with Child Workflows](/design-patterns/fanout-child-workflows) | Up to ~500K records | Fixed concurrency (one child per chunk) | No |
| [Batch Iterator](/design-patterns/batch-iterator) | Unlimited | Limited (activities per page) | Yes — fixed page rate |
| [Sliding Window](/design-patterns/sliding-window) | Unlimited | Bounded window of concurrent children | Yes — configurable window |
| [MapReduce Tree](/design-patterns/mapreduce-tree) | Unlimited | Fully parallel recursive tree | No — maximum speed |
Expand All @@ -26,7 +28,7 @@ These patterns process large volumes of records reliably, at scale, and without
href: "/design-patterns/fanout-child-workflows",
icon: "fanout-child-workflows-icon.svg",
title: "Fan-Out with Child Workflows",
description: "Splits a record set into fixed-size chunks and assigns each to an independent child Workflow. Direct to reason about; best for record sets up to ~4M items.",
description: "Splits a record set into fixed-size chunks and assigns each to an independent child Workflow. Direct to reason about; best for record sets up to ~500K items.",
},
{
href: "/design-patterns/batch-iterator",
Expand Down
10 changes: 9 additions & 1 deletion docs/design-patterns/child-workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ id: child-workflows
title: "Child Workflows Pattern"
sidebar_label: "Child Workflows"
description: "Decomposes complex Workflows into smaller, reusable units. Each child has an independent Workflow ID, history, and lifecycle."
tags:
- Design Patterns
- Child Workflows
---

import Tabs from '@theme/Tabs';
Expand Down Expand Up @@ -764,7 +767,7 @@ Starting a Child Workflow has more overhead than starting an Activity.
## Common pitfalls

- **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.
- **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. Use fixed-size batches or a sliding window.
- **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. Temporal enforces a hard limit of 2,000 pending (in-flight) children per parent, but the [recommended cap](/child-workflows#when-to-use-child-workflows) is lower: a single parent should not spawn more than 1,000 Child Workflow Executions in total, since each one adds more history to the parent than an Activity would. Use fixed-size batches or a sliding window.
- **Ignoring the Parent Close Policy.** The default policy is TERMINATE, which kills children when the parent closes. If children must outlive the parent, set the policy to ABANDON explicitly.
- **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.
- **Omitting Workflow IDs.** 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.
Expand All @@ -778,6 +781,11 @@ Starting a Child Workflow has more overhead than starting an Activity.
- **[Continue-As-New](/design-patterns/continue-as-new)**: Child Workflows can use Continue-As-New independently.
- **[Saga Pattern](/design-patterns/saga-pattern)**: Children as compensatable transactions.

### References

- [Parent Close Policy](/parent-close-policy): Canonical reference for `TERMINATE`, `ABANDON`, and `REQUEST_CANCEL`, including the default and how each behaves during a Continue-As-New.
- [Child Workflows (Go)](/develop/go/workflows/child-workflows) · [Child Workflows (Java)](/develop/java/workflows/child-workflows) · [Child Workflows (Python)](/develop/python/workflows/child-workflows) · [Child Workflows (TypeScript)](/develop/typescript/workflows/child-workflows): Official per-SDK how-to guides.

### Sample code

**Java:**
Expand Down
9 changes: 6 additions & 3 deletions docs/design-patterns/continue-as-new.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ id: continue-as-new
title: "Continue-As-New Pattern"
sidebar_label: "Continue-As-New"
description: "Prevents unbounded history growth by completing the current execution and starting a new one with fresh history."
tags:
- Design Patterns
- Workflows
---

import Tabs from '@theme/Tabs';
Expand All @@ -16,9 +19,9 @@ By archiving old event history and starting fresh, Continue-As-New also reduces

## Problem

In long-running Workflows, you often need to execute periodic tasks indefinitely, process unbounded streams of data without accumulating history, implement infinite loops that run for months or years, avoid hitting the 50,000 event history limit, and maintain Workflow state across logical restarts.
Long-running Workflows — periodic tasks that run indefinitely, infinite loops spanning months or years, or Workflows processing an unbounded stream of data — accumulate Event History with every iteration. Left unchecked, that history eventually hits the 51,200-event limit, and the Workflow still needs to keep its state across whatever comes next.

Without Continue-As-New, you must manually stop and restart Workflows (losing continuity), risk hitting history limits and Workflow failures, implement external orchestration to manage Workflow lifecycle, and accept degraded performance as history grows large.
Without Continue-As-New, the alternatives are all worse: manually stop and restart Workflows and lose continuity, build external orchestration to manage the Workflow's lifecycle, or accept degraded performance as history grows — and risk failure once it hits the limit.

## Solution

Expand Down Expand Up @@ -394,7 +397,7 @@ You cannot undo Continue-As-New once triggered.
- **Version carefully.** Ensure new code can handle state from old executions.
- **Monitor history size.** Track event count and continue before hitting limits.
- **Use typed APIs.** In Java, prefer `newContinueAsNewStub()` over untyped `continueAsNew()`. In TypeScript, use the generic `continueAsNew<typeof myWorkflow>()` for type safety.
- **Consider cron.** For fixed Schedules, use Temporal Schedules instead.
- **Consider cron.** For fixed Schedules, use [Temporal Schedules](/schedule) instead.
- **Test state transfer.** Verify state correctly passes between executions.

## Common pitfalls
Expand Down
10 changes: 8 additions & 2 deletions docs/design-patterns/delayed-callback.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
id: delayed-callback
title: "Delayed Callback (Webhooks)"
sidebar_label: "Delayed Callback"
description: "Integrates webhooks durably: receive inbound webhooks via Signals, fire delayed outbound callbacks with durable timers, and complete Activities asynchronously via task tokens."
description: "Webhooks become durable: inbound calls arrive as Signals, outbound calls fire after a durable sleep, and Activities complete later via task tokens."
tags:
- Design Patterns
- Signals
---

import Tabs from '@theme/Tabs';
Expand Down Expand Up @@ -753,4 +756,7 @@ func CompleteJob(ctx context.Context, c client.Client, jobID string, result stri
- [Polling External Services](/design-patterns/polling) — alternative to callbacks when the external system does not support webhooks
- [Delayed Start](/design-patterns/delayed-start) — defer Workflow execution to a future time without `workflow.sleep()`
- [Long-Running Activity](/design-patterns/long-running-activity) — heartbeating pattern for activities that run for extended periods
- **In the future** - Org-to-Org Nexus, stay tuned.

### References

- [Asynchronous Activity Completion](/activity-execution#asynchronous-activity-completion) — canonical reference for Pattern 3's task-token mechanism, including when to prefer it over Signals
158 changes: 157 additions & 1 deletion docs/design-patterns/delayed-retry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
id: delayed-retry
title: "Delayed Retry"
sidebar_label: "Delayed Retry"
description: "Override the next retry interval for a specific failure using nextRetryDelay on ApplicationFailure. Use when an error carries information about how long to wait before retrying."
description: "Override one failure's retry interval with nextRetryDelay on ApplicationFailure, matching the wait time the error reports."
tags:
- Design Patterns
- Errors
---

import Tabs from '@theme/Tabs';
Expand Down Expand Up @@ -66,6 +69,61 @@ Extract the wait duration from the error or response and pass it to `Application
The RetryPolicy's `MaximumAttempts` and `ScheduleToCloseTimeout` still apply — only the interval for the next retry is overridden.

<Tabs groupId="language" queryString>
<TabItem value="python" label="Python">

```python
# activities.py
from datetime import timedelta
from temporalio import activity
from temporalio.exceptions import ApplicationError

@activity.defn
async def call_api(endpoint: str) -> str:
response = await http_client.get(endpoint)

if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
if retry_after is not None:
raise ApplicationError(
f"Rate limited — retrying after {retry_after}s",
type="RateLimitError",
next_retry_delay=timedelta(seconds=int(retry_after)),
)
raise ApplicationError(
"Rate limited — retrying per RetryPolicy", type="RateLimitError"
)

return response.text
```

</TabItem>
<TabItem value="go" label="Go">

```go
// rate_limited_activity.go
func CallApi(ctx context.Context, endpoint string) (string, error) {
response, err := httpClient.Get(endpoint)
if err != nil {
return "", err
}

if response.StatusCode == 429 {
if retryAfter := response.Header.Get("Retry-After"); retryAfter != "" {
seconds, _ := strconv.Atoi(retryAfter)
return "", temporal.NewApplicationErrorWithOptions(
fmt.Sprintf("Rate limited — retrying after %ds", seconds),
"RateLimitError",
temporal.ApplicationErrorOptions{NextRetryDelay: time.Duration(seconds) * time.Second},
)
}
return "", temporal.NewApplicationError("Rate limited — retrying per RetryPolicy", "RateLimitError")
}

return response.Body, nil
}
```

</TabItem>
<TabItem value="java" label="Java">

```java
Expand Down Expand Up @@ -132,6 +190,54 @@ export async function callApi(endpoint: string): Promise<string> {
You can also set the delay dynamically based on the attempt number — for example, to implement a custom backoff that differs from exponential, or to add a known base delay on top of the standard backoff.

<Tabs groupId="language" queryString>
<TabItem value="python" label="Python">

```python
# activities.py
from datetime import timedelta
from temporalio import activity
from temporalio.exceptions import ApplicationError

@activity.defn
async def process(input: str) -> str:
attempt = activity.info().attempt

try:
return await downstream_service.call(input)
except ServiceUnavailableError as e:
# Custom delay: 3 seconds × attempt number (3s, 6s, 9s, …)
raise ApplicationError(
f"Service unavailable on attempt {attempt}",
type="ServiceUnavailable",
next_retry_delay=timedelta(seconds=3 * attempt),
) from e
```

</TabItem>
<TabItem value="go" label="Go">

```go
// backoff_activity.go
func Process(ctx context.Context, input string) (string, error) {
attempt := activity.GetInfo(ctx).Attempt

result, err := downstreamService.Call(input)
if err != nil {
// Custom delay: 3 seconds × attempt number (3s, 6s, 9s, …)
return "", temporal.NewApplicationErrorWithOptions(
fmt.Sprintf("Service unavailable on attempt %d", attempt),
"ServiceUnavailable",
temporal.ApplicationErrorOptions{
Cause: err,
NextRetryDelay: 3 * time.Second * time.Duration(attempt),
},
)
}
return result, nil
}
```

</TabItem>
<TabItem value="java" label="Java">

```java
Expand Down Expand Up @@ -193,6 +299,56 @@ The Workflow sets a normal `RetryPolicy`.
The `nextRetryDelay` set in the Activity overrides the interval only for the retry following that specific failure — subsequent attempts fall back to the RetryPolicy schedule if `nextRetryDelay` is not set again.

<Tabs groupId="language" queryString>
<TabItem value="python" label="Python">

```python
# workflows.py
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy

with workflow.unsafe.imports_passed_through():
from activities import call_api

@workflow.defn
class ApiWorkflow:
@workflow.run
async def run(self, endpoint: str) -> str:
return await workflow.execute_activity(
call_api,
endpoint,
start_to_close_timeout=timedelta(seconds=10),
retry_policy=RetryPolicy(
initial_interval=timedelta(seconds=1),
backoff_coefficient=2.0,
maximum_attempts=10,
),
)
```

</TabItem>
<TabItem value="go" label="Go">

```go
// api_workflow.go
func ApiWorkflow(ctx workflow.Context, endpoint string) (string, error) {
ao := workflow.ActivityOptions{
StartToCloseTimeout: 10 * time.Second,
RetryPolicy: &temporal.RetryPolicy{
InitialInterval: time.Second,
BackoffCoefficient: 2.0,
MaximumAttempts: 10,
},
}
ctx = workflow.WithActivityOptions(ctx, ao)

var result string
err := workflow.ExecuteActivity(ctx, CallApi, endpoint).Get(ctx, &result)
return result, err
}
```

</TabItem>
<TabItem value="java" label="Java">

```java
Expand Down
Loading