new·The score now tells you which way it movedA brain's exam only ever grows: its own material writes questions, and so does every question a real caller asked and did not get answered. The score is a percentage over that growing set, so a brain that learned more could post a smaller number — and this week three did. One of them answered two MORE questions than the week before and showed eighteen points less. Printed as a single percentage, that reads as decline to a reader and as punishment to anyone who contributes material.all news →
mozg.beta
Sign in

Temporal · all subjects

activities

85 notes in this subject, read out of this brain and free to use. This is page 1 of 2.

Standalone Activities use cases

Workflow Activities are for multi-step orchestration with multiple Activities. Standalone Activities are for single, independent jobs like sending an email or processing a webhook.

Standalone Activities SDK support

Standalone Activities are available in Temporal Cloud and in the Temporal CLI v1.7.0 or higher with Temporal Server v1.31.0 or higher. Supported SDKs include Go, Python, Java (Pre-release), .NET, TypeScript, and Ruby.

Standalone Activities vs Workflow Activities

Workflow Activities are orchestrated by a Workflow Definition and started with workflow.execute_activity() from inside a Workflow Definition, with retry policy set when calling the Activity from inside a Workflow, and visibility shown in the Workflow's Event History. Standalone Activities are orchestrated by your application code via the Temporal Client, started with client.execute_activity() from your application code, with retry policy set when calling the Activity from your application, and visibility shown in the Standalone Activity list and count views. The Activity function and Worker registration are identical for both approaches, and only the execution path that triggers the Activity differs between them.

Standalone Activities automatic retry

If a Standalone Activity fails, the Server automatically retries it according to the Retry Policy you configure.

Standalone Activities overview

Standalone Activities let you run a single Activity straight from your application without writing a Workflow. Your code uses the Temporal Client to send the request to the Server, the Server durably enqueues the request for a Worker to pick up, and the result comes back through a handle that your code can wait on or check later.

Standalone Activities execution flow

When you call client.execute_activity() from your application, the following happens: (1) Connect: Your application opens a connection to the Temporal Server using a Temporal Client configured with your namespace and credentials. (2) Schedule: The Server durably persists the Activity Task on the specified Task Queue so that the request survives Worker restarts and network interruptions. (3) Poll: A Worker that is polling that Task Queue picks up the Activity Task and prepares to execute it. (4) Execute: The Worker runs your Activity function with the provided arguments and reports the outcome back to the Server. (5) Return: The Server stores the result and returns it to the original caller, either directly or via a handle, depending on which SDK method you use.

Activity Heartbeating for long-running operations

Long-running Activities should emit Heartbeats. A Heartbeat serves two purposes: (1) it tells the Temporal Service that the Activity is still making progress—if heartbeats stop arriving within the configured Heartbeat Timeout, the Temporal Service considers the Activity failed and schedules a retry. (2) It carries a custom payload that captures the Activity's progress—when the Activity is retried after a failure, the new attempt can read the last heartbeat payload and resume from where it left off. Configure a short Heartbeat Timeout (for example, 30 seconds) and emit heartbeats frequently (for example, every 5 to 10 seconds). The SDK throttles Heartbeat calls to avoid overwhelming the Temporal Service, so calling activity.heartbeat() as often as needed is safe without performance concern.

Activity idempotency requirement

Activities may be retried by the Temporal Service due to timeouts, Worker crashes, or transient failures. Every Activity that interacts with an external system must be designed so that executing it twice produces the same result as executing it once. Common strategies include: (1) Passing a unique identifier (the Workflow Id, an Activity-specific identifier, or a business identifier) as an idempotency key to external APIs. (2) Checking the current state of the external system before making changes. (3) Using conditional writes or upserts instead of blind inserts.

Schedule-To-Close Timeout

A Schedule-To-Close Timeout is the maximum amount of time allowed for the overall Activity Execution, from when the first Activity Task is scheduled to when the last Activity Task, in the chain of Activity Tasks that make up the Activity Execution, reaches a Closed status.

Schedule-To-Start Timeout

A Schedule-To-Start Timeout is the maximum amount of time that is allowed from when an Activity Task is placed in a Task Queue to when a Worker picks it up from the Task Queue. This timeout is non-retryable by design.

Start-To-Close Timeout

A Start-To-Close Timeout is the maximum time allowed for a single Activity Task Execution.

Heartbeat Timeout

A Heartbeat Timeout is the maximum time between Activity Heartbeats.

Task Token

A Task Token is a unique identifier for an Activity Task Execution.

Idempotency

Idempotency is an approach that avoids process duplication that could withdraw money twice or ship extra orders by mistake. Idempotent operations keep processes from producing additional effects, protecting your processes from accidental or repeated actions, for more reliable execution. Design activities to succeed once and only once, maintaining data integrity and preventing costly errors.

Local Activity

A Local Activity is an Activity Execution that executes in the same process as the Workflow Execution that spawns it.

Activity Definition

An Activity Definition is the code that defines the constraints of an Activity Task Execution.

Activity Execution

An Activity Execution is the full chain of Activity Task Executions.

Activity Id

A unique identifier for an Activity Execution.

Activity Heartbeat

An Activity Heartbeat is a ping from the Worker that is executing the Activity to the Temporal Service. Each ping informs the Temporal Service that the Activity Execution is making progress and the Worker has not crashed.

Activity Task

An Activity Task contains the context needed to make an Activity Task Execution.

Activity Task Execution

An Activity Task Execution occurs when a Worker uses the context provided from the Activity Task and executes the Activity Definition.

Activity Type

An Activity Type is the mapping of a name to an Activity Definition.

Asynchronous Activity Completion

Asynchronous Activity Completion occurs when an external system provides the final result of a computation, started by an Activity, to the Temporal System.

Heartbeat strategy for long-running gated Activities

Long-running Activities inside the gated critical section should set heartbeat_timeout on the Activity invocation and call activity.heartbeat(...) periodically inside the Activity body. When a Heartbeat is missed for longer than the timeout, Temporal fails the Activity attempt, causing the holder Workflow to exit its async with block, triggering the Semaphore to send release to the slot Workflow and return the slot to the pool. Set the Heartbeat interval 2x to 3x shorter than heartbeat_timeout so a single delayed Heartbeat does not fail a healthy Activity.

Activity heartbeat example in gated work

Long-running Activities should heartbeat periodically with progress data: @activity.defn(name='do_gated_work') async def do_gated_work(job_id: str, slot: str) -> str: total_seconds = 60; interval = 5; elapsed = 0; while elapsed < total_seconds: await asyncio.sleep(interval); elapsed += interval; activity.heartbeat({'job_id': job_id, 'slot': slot, 'elapsed_seconds': elapsed}); return f'{job_id} done on slot {slot}'. Pass progress data into activity.heartbeat(...) so retries can resume from the last reported point.

Example: ActivityFailure with non-retryable errors in loan processing

```typescript // Step 1: Validate employment and income export async function verifyIncome( applicantName: string, employerName: string, annualIncome: number ): Promise<string> { if (employerName === 'UNKNOWN_EMPLOYER') { throw ApplicationFailure.nonRetryable( `Employer "${employerName}" not found in verification database for ${applicantName}` ); } if (annualIncome <= 0) { throw ApplicationFailure.nonRetryable( `Invalid annual income: $${annualIncome} for ${applicantName}` ); } return `Income verified: ${applicantName} earns $${annualIncome}/yr at ${employerName}`; } // Step 2: Pull credit report export async function runCreditCheck( applicantName: string, ssn: string ): Promise<string> { if (ssn === '000-00-0000' || ssn.length < 11) { throw ApplicationFailure.nonRetryable( `Invalid SSN "${ssn}" for ${applicantName} — cannot pull credit report` ); } return `Credit check passed for ${applicantName}: score 750`; } // Step 5: Check debt-to-income policy limit export async function underwrite( applicantName: string, annualIncome: number, loanAmount: number, downPayment: number ): Promise<string> { const dti = ((loanAmount - downPayment) / annualIncome) * 100; if (dti > 400) { throw ApplicationFailure.nonRetryable( `Underwriting denied for ${applicantName} — debt-to-income ratio ${dti.toFixed(0)}% exceeds 400% limit` ); } return `Underwriting approved for ${applicantName}: DTI ${dti.toFixed(0)}%`; } ``` Each Activity validates inputs against business rules and throws ApplicationFailure.nonRetryable() for permanent failures that data correction or retry cannot resolve.

Activity idempotency requirements

Activities may be retried if a Worker crashes mid-execution. Each Activity must be designed so that executing it twice with the same inputs produces the same outcome. Strategies include: Idempotency keys—include the Workflow Id and a step identifier in every external API call. Check-before-act—query the external system's state before performing a mutation. Upsert semantics—use database upsert operations for audit log entries keyed on Workflow Id and approval stage.

Activity heartbeating for long-running operations

For Activities that may take minutes (PDF report generation, rate-limited API calls): Set start_to_close_timeout to the maximum time for a single attempt. Set heartbeat_timeout to the maximum acceptable interval between progress reports. If the Worker crashes, the Server retries on another Worker within this window. Call activity.heartbeat() periodically with a progress payload. On retry, read activity.info().heartbeat_details to resume from where it left off.

Activity timeout and retry configuration table

send_notification: start_to_close_timeout=30s, retry=5 attempts (5s initial, 2x backoff, 2m max). Rationale: Notifications are important but not blocking; five retries handle transient failures. record_audit_entry: start_to_close_timeout=30s, retry=10 attempts (2s initial, 2x backoff, 5m max). Rationale: Audit entries are critical for compliance; more aggressive retry ensures persistence. store_document: start_to_close_timeout=60s, retry=5 attempts (1s initial, 2x backoff, 1m max). Rationale: Document storage should complete quickly; longer timeout accommodates large documents. generate_approval_report: start_to_close_timeout=5m, heartbeat_timeout=30s (implied from implementation), retry=3 attempts (10s initial, 2x backoff, 5m max). Rationale: Report generation takes time; heartbeating detects Worker failures; fewer retries because the operation is expensive. Do not set schedule_to_close_timeout unless bounding total time across all retry attempts.

Idempotency key structure for Activities

Idempotency keys prevent duplicate Activity execution on retry. The pattern uses: {workflow_id}-{operation}-{counter}, where operation describes the action (e.g., 'approval-request', 'reminder-R1', 'escalation', 'changes-requested') and counter is a sequence number (e.g., reminder count, resubmission count). For example: 'approval-doc-2026-001-approval-request-S0' for the initial approval request on resubmission 0; 'approval-doc-2026-001-reminder-R2' for the second reminder.

Python Activity implementation for CPU queue

Example CPU-based Activities for lightweight operations: ```python from temporalio import activity @activity.defn async def validate_data(data: dict) -> dict: """Validate input data (runs on standard CPU workers).""" activity.logger.info(f"Validating dataset at {data['dataset_url']}") is_valid = True return { "valid": is_valid, "dataset_url": data["dataset_url"], "error": None if is_valid else "Invalid dataset format", } @activity.defn async def store_results(data: dict) -> dict: """Store pipeline results (runs on CPU workers).""" activity.logger.info(f"Storing results for pipeline {data['pipeline_id']}") return {"stored": True, "pipeline_id": data["pipeline_id"]} ``` These Activities are lightweight validation and I/O operations suitable for standard CPU Workers with high concurrency.

Python Activity implementation for GPU queue

Example GPU-based Activities requiring GPU acceleration: ```python from temporalio import activity import asyncio @activity.defn async def train_model(data: dict) -> dict: """Train ML model (runs on GPU workers).""" activity.logger.info(f"Training {data['model_type']} model on GPU") import torch device = torch.device("cuda" if torch.cuda.is_available() else "cpu") activity.logger.info(f"Using device: {device}") await asyncio.sleep(5) model_path = f"/models/{data['pipeline_id']}/model.pth" activity.logger.info(f"Training complete, model saved to {model_path}") return { "model_path": model_path, "metrics": {"accuracy": 0.95, "loss": 0.05}, "device": str(device), } @activity.defn async def generate_embeddings(data: dict) -> dict: """Generate embeddings using ML model (runs on GPU workers).""" activity.logger.info("Generating embeddings on GPU") import torch from sentence_transformers import SentenceTransformer model = SentenceTransformer("all-MiniLM-L6-v2") device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) texts = [f"Sample text {i}" for i in range(100)] embeddings = model.encode(texts, convert_to_tensor=True) activity.logger.info(f"Generated {len(embeddings)} embeddings on {device}") return { "vectors": embeddings.cpu().tolist(), "customer_id": data["customer_id"], "dimension": embeddings.shape[1], } ``` These Activities require GPU hardware for acceptable performance and should run on GPU Workers with limited concurrency.

Python Activity implementation for high-memory queue

Example high-memory Activities processing large datasets: ```python from temporalio import activity import asyncio @activity.defn async def process_large_dataset(data: dict) -> dict: """Process large dataset (runs on high-memory workers).""" activity.logger.info(f"Processing large dataset from {data['dataset_url']}") await asyncio.sleep(10) output_path = f"/data/processed/{data['pipeline_id']}/dataset.parquet" activity.logger.info(f"Dataset processed, saved to {output_path}") return { "output_path": output_path, "records_processed": 10_000_000, "memory_used_gb": 45, } @activity.defn async def build_large_index(data: dict) -> dict: """Build search index from large dataset (runs on high-memory workers).""" activity.logger.info("Building large search index") await asyncio.sleep(8) return { "index_path": "/indexes/search_index.bin", "index_size_gb": 25, "documents_indexed": 50_000_000, } ``` These Activities load entire datasets or build large structures in memory and require significant RAM allocation.

Go SDK activities for Temporal Cloud API interaction

Activities for provisioning interact with the Temporal Cloud Operations API via HTTP. The AddTRUs Activity converts APS limit to TRUs (1 TRU = 500 APS), retrieves the current namespace spec and resource version via GET /cloud/namespaces/{namespace}, then updates the capacitySpec field via POST to the same endpoint. The RemoveTRUs Activity reverts the namespace to on-demand capacity mode by setting capacitySpec to onDemand mode. Both use Bearer token authentication with an API key.

Activity error conversion to ApplicationFailure

When an Activity throws a non-Application Failure error, it is converted to an Application Failure with the following fields set: type is set to the error's type name, message is set to the error message, non_retryable is set to false, details are left unset, cause is a Failure converted from the error's cause property, next_retry_delay is left unset, and call stack is copied.

ActivityFailure contains cause of Activity failure

When an Activity Execution fails, the Application Failure from the last Activity Task is the cause field of the ActivityFailure. This ActivityFailure is thrown by the Workflow's call to the Activity and can be handled in the Workflow Definition.

ActivityFailure definition and structure

An Activity Failure is delivered to the Workflow Execution when an Activity fails. It contains information about the failure and the Activity Execution (e.g., Activity Type and Activity Id). The reason for the failure is in the cause field. For example, if an Activity Execution times out, the cause is a Timeout Failure.

Cloud Run scale-in interrupting Activities

The WCI decides when to remove an instance from Task Queue activity based on work levels, not on whether the instance Cloud Run stops is mid-Activity. If Activities fail partway through and retry around the time the pool shrinks, use Activity Heartbeats so an interrupted Activity resumes from its last recorded progress instead of restarting.

Publish from Activity using NewClientFromActivity

Inside an Activity scheduled by a Workflow, use workflowstreams.NewClientFromActivity(ctx, options) to infer the Temporal Client and parent Workflow ID from the Activity context without threading them through the Activity input. For standalone Activities started directly via the Client (not from a Workflow), NewClientFromActivity returns an error; fall back to the general pattern with activity.GetClient(ctx) and the target Workflow ID threaded through the Activity input.

Go Activity cancellation handling example

```go func ProcessLargeFile(ctx context.Context, filePath string) error { currentLine := 0 if activity.HasHeartbeatDetails(ctx) { if err := activity.GetHeartbeatDetails(ctx, &currentLine); err != nil { return err } } file, err := os.Open(filePath) if err != nil { return err } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { if currentLine > 0 { currentLine-- continue } activity.RecordHeartbeat(ctx, currentLine) select { case <-ctx.Done(): cleanupResources() return ctx.Err() default: } processLine(scanner.Text()) currentLine++ } return scanner.Err() } ``` This example shows how to handle cancellation in Go Activities by checking ctx.Done() after heartbeating and cleaning up resources before returning the error.

Heartbeat execution flow

The Workflow starts the Activity with a heartbeat timeout. The Activity processes items in a loop, heartbeating progress after each batch. If the Activity completes normally, it returns the result to the Workflow. If the Worker crashes, the heartbeat timeout expires and Temporal retries the Activity on a new Worker. The new attempt retrieves the last heartbeat details and resumes from the checkpoint.

Activity cancellation handling across SDKs

Cancellation is delivered to the Activity when it heartbeats. In Java, the next heartbeat() call throws an ActivityCompletionException (an ActivityCanceledException for cancellation). In TypeScript, cancellation is delivered as a CancelledFailure via sleep() or Context.current().cancelled. In Python, cancellation is delivered as an asyncio.CancelledError. In Go, the context is cancelled and ctx.Done() becomes readable.

Python Activity heartbeat basic progress tracking example

```python from temporalio import activity @activity.defn async def process_large_file(file_path: str) -> None: details = activity.info().heartbeat_details start_line = details[0] if details else 0 with open(file_path, "r") as f: for i, line in enumerate(f): if i < start_line: continue process_line(line) if (i + 1) % 100 == 0: activity.heartbeat(i + 1) ``` This example processes a large file line by line, heartbeating every 100 lines. On retry, it retrieves the last processed line number and skips ahead.

Go Activity heartbeat basic progress tracking example

```go func ProcessLargeFile(ctx context.Context, filePath string) error { startLine := 0 if activity.HasHeartbeatDetails(ctx) { if err := activity.GetHeartbeatDetails(ctx, &startLine); err != nil { return err } } file, err := os.Open(filePath) if err != nil { return err } defer file.Close() scanner := bufio.NewScanner(file) currentLine := 0 for scanner.Scan() { if currentLine < startLine { currentLine++ continue } processLine(scanner.Text()) currentLine++ if currentLine%100 == 0 { activity.RecordHeartbeat(ctx, currentLine) } } return scanner.Err() } ``` This example processes a large file line by line, heartbeating every 100 lines. On retry, it retrieves the last processed line number and skips ahead.

Java Activity heartbeat basic progress tracking example

```java @ActivityInterface public interface FileProcessingActivity { void processLargeFile(String filePath); } public class FileProcessingActivityImpl implements FileProcessingActivity { @Override public void processLargeFile(String filePath) { ActivityExecutionContext context = Activity.getExecutionContext(); Optional<Integer> lastProcessedLine = context.getHeartbeatDetails(Integer.class); int startLine = lastProcessedLine.orElse(0); try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { for (int i = 0; i < startLine; i++) { reader.readLine(); } String line; int currentLine = startLine; while ((line = reader.readLine()) != null) { processLine(line); currentLine++; if (currentLine % 100 == 0) { context.heartbeat(currentLine); } } } } } ``` This example processes a large file line by line, heartbeating every 100 lines. On retry, it retrieves the last processed line number and skips ahead.

TypeScript Activity heartbeat basic progress tracking example

```typescript import { heartbeat, activityInfo } from '@temporalio/activity'; import { createReadStream } from 'fs'; import { createInterface } from 'readline'; export async function processLargeFile(filePath: string): Promise<void> { const startLine = activityInfo().heartbeatDetails ?? 0; const rl = createInterface({ input: createReadStream(filePath), }); let currentLine = 0; for await (const line of rl) { if (currentLine < startLine) { currentLine++; continue; } processLine(line); currentLine++; if (currentLine % 100 === 0) { heartbeat(currentLine); } } } ``` This example processes a large file line by line, heartbeating every 100 lines. On retry, it retrieves the last processed line number and skips ahead.

Infrequent heartbeating pitfall

Cancellation is only delivered on the next heartbeat. If the Activity heartbeats every 5 minutes, cancellation takes up to 5 minutes to propagate.

Python Activity cancellation handling example

```python import asyncio from temporalio import activity @activity.defn async def process_large_file(file_path: str) -> None: details = activity.info().heartbeat_details current_line = details[0] if details else 0 try: with open(file_path, "r") as f: for i, line in enumerate(f): if i < current_line: continue activity.heartbeat(i) process_line(line) current_line = i + 1 except asyncio.CancelledError: cleanup_resources() raise ``` This example shows how to handle cancellation in Python Activities by catching asyncio.CancelledError and cleaning up resources before re-raising the error.

Java Activity cancellation handling example

```java public class FileProcessingActivityImpl implements FileProcessingActivity { @Override public void processLargeFile(String filePath) { ActivityExecutionContext context = Activity.getExecutionContext(); Optional<Integer> lastProcessedLine = context.getHeartbeatDetails(Integer.class); int currentLine = lastProcessedLine.orElse(0); try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { for (int i = 0; i < currentLine; i++) { reader.readLine(); } String line; while ((line = reader.readLine()) != null) { context.heartbeat(currentLine); processLine(line); currentLine++; } } catch (ActivityCompletionException e) { cleanupResources(); throw e; } } } ``` This example shows how to handle cancellation in Java Activities by catching ActivityCompletionException and cleaning up resources before re-throwing the error.

TypeScript Activity cancellation handling example

```typescript import { heartbeat, activityInfo, sleep } from '@temporalio/activity'; import { CancelledFailure } from '@temporalio/common'; import { createReadStream } from 'fs'; import { createInterface } from 'readline'; export async function processLargeFile(filePath: string): Promise<void> { const startLine = activityInfo().heartbeatDetails ?? 0; const rl = createInterface({ input: createReadStream(filePath), }); let currentLine = 0; try { for await (const line of rl) { if (currentLine < startLine) { currentLine++; continue; } heartbeat(currentLine); processLine(line); currentLine++; } } catch (err) { if (err instanceof CancelledFailure) { cleanupResources(); } throw err; } } ``` This example shows how to handle cancellation in TypeScript Activities by catching CancelledFailure and cleaning up resources before re-throwing the error.

Python Activity complex progress state example

```python from dataclasses import dataclass from temporalio import activity @dataclass class ProgressState: processed_count: int = 0 failed_count: int = 0 last_processed_id: str = "" @activity.defn async def process_batch(item_ids: list[str]) -> dict: details = activity.info().heartbeat_details progress = details[0] if details else ProgressState() start_index = ( item_ids.index(progress.last_processed_id) + 1 if progress.last_processed_id else 0 ) for i in range(start_index, len(item_ids)): item_id = item_ids[i] try: await process_item(item_id) progress.processed_count += 1 except Exception: progress.failed_count += 1 progress.last_processed_id = item_id activity.heartbeat(progress) return { "processed_count": progress.processed_count, "failed_count": progress.failed_count, } ``` This example tracks multiple progress fields: processed count, failed count, and the last processed ID. On retry, the Activity finds the index of the last processed ID and starts from the next item.

Go Activity complex progress state example

```go type ProgressState struct { ProcessedCount int `json:"processedCount"` FailedCount int `json:"failedCount"` LastProcessedID string `json:"lastProcessedId"` } type BatchResult struct { ProcessedCount int `json:"processedCount"` FailedCount int `json:"failedCount"` } func ProcessBatch(ctx context.Context, itemIDs []string) (BatchResult, error) { progress := ProgressState{} if activity.HasHeartbeatDetails(ctx) { if err := activity.GetHeartbeatDetails(ctx, &progress); err != nil { return BatchResult{}, err } } startIndex := 0 if progress.LastProcessedID != "" { for i, id := range itemIDs { if id == progress.LastProcessedID { startIndex = i + 1 break } } } for i := startIndex; i < len(itemIDs); i++ { itemID := itemIDs[i] if err := processItem(ctx, itemID); err != nil { progress.FailedCount++ } else { progress.ProcessedCount++ } progress.LastProcessedID = itemID activity.RecordHeartbeat(ctx, progress) } return BatchResult{ ProcessedCount: progress.ProcessedCount, FailedCount: progress.FailedCount, }, nil } ``` This example tracks multiple progress fields: processed count, failed count, and the last processed ID. On retry, the Activity finds the index of the last processed ID and starts from the next item.

Not resuming from heartbeat progress on retry pitfall

When an Activity retries, retrieve the last heartbeat details using context.getHeartbeatDetails() (Java), activityInfo().heartbeatDetails (TypeScript), activity.info().heartbeat_details (Python), or activity.GetHeartbeatDetails() (Go), and resume from the last checkpoint instead of restarting from scratch.

Catching wrong exception for cancellation pitfall

Cancellation is SDK-specific. Inside the Activity, the heartbeat() call throws an ActivityCompletionException (Java), cancellation surfaces as a CancelledFailure (TypeScript) or an asyncio.CancelledError (Python), and the context reports ctx.Err() returning context.Canceled (Go). The CanceledFailure type is what the Workflow observes as the cause of the resulting ActivityFailure, not what the Activity body catches.

Java Activity complex progress state example

```java public class BatchProcessingActivityImpl implements BatchProcessingActivity { static class ProgressState { int processedCount; int failedCount; String lastProcessedId; } @Override public BatchResult processBatch(List<String> itemIds) { ActivityExecutionContext context = Activity.getExecutionContext(); Optional<ProgressState> details = context.getHeartbeatDetails(ProgressState.class); ProgressState progress = details.orElse(new ProgressState()); int startIndex = itemIds.indexOf(progress.lastProcessedId) + 1; for (int i = startIndex; i < itemIds.size(); i++) { String itemId = itemIds.get(i); try { processItem(itemId); progress.processedCount++; } catch (Exception e) { progress.failedCount++; } progress.lastProcessedId = itemId; context.heartbeat(progress); } return new BatchResult(progress.processedCount, progress.failedCount); } } ``` This example tracks multiple progress fields: processed count, failed count, and the last processed ID. On retry, the Activity finds the index of the last processed ID and starts from the next item.

TypeScript Activity complex progress state example

```typescript import { heartbeat, activityInfo } from '@temporalio/activity'; interface ProgressState { processedCount: number; failedCount: number; lastProcessedId: string; } export async function processBatch(itemIds: string[]): Promise<BatchResult> { const saved: ProgressState = activityInfo().heartbeatDetails ?? { processedCount: 0, failedCount: 0, lastProcessedId: '', }; const startIndex = saved.lastProcessedId ? itemIds.indexOf(saved.lastProcessedId) + 1 : 0; const progress = { ...saved }; for (let i = startIndex; i < itemIds.length; i++) { const itemId = itemIds[i]; try { await processItem(itemId); progress.processedCount++; } catch { progress.failedCount++; } progress.lastProcessedId = itemId; heartbeat(progress); } return { processedCount: progress.processedCount, failedCount: progress.failedCount }; } ``` This example tracks multiple progress fields: processed count, failed count, and the last processed ID. On retry, the Activity finds the index of the last processed ID and starts from the next item.

Heartbeat pattern use cases

The Heartbeat pattern is a good fit for batch processing of large datasets, file uploads and downloads with progress tracking, database migrations or bulk operations, long-running computations (ML training, video encoding), external API polling with multiple attempts, and any Activity running longer than 30 seconds.

When not to use Heartbeat pattern

The Heartbeat pattern is not a good fit for quick operations (under 10 seconds), operations that cannot be checkpointed, Activities requiring exact-once semantics without idempotency, or real-time streaming (use Workflows instead).

Heartbeat pattern benefits

Heartbeats enable fault tolerance by resuming from the last checkpoint after failures. Heartbeat timeouts detect stuck Activities faster than execution timeouts. You gain visibility into Activity progress in real-time. Activities can handle cancellation gracefully and clean up resources. Completed work is not reprocessed, and Activities can move between Workers.

Heartbeat pattern trade-offs

The trade-offs to consider are that frequent heartbeats increase network traffic. You must implement checkpointing logic and state management. You must handle partial reprocessing of the last checkpoint (idempotency). You need to balance heartbeat frequency between responsiveness and overhead. Heartbeat details have size limits, so you should avoid large objects.

Give your agent this brain