diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5297deb..b8d42c3 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,6 +22,8 @@
+
+
diff --git a/README.md b/README.md
index e3c7ce1..cb81c3c 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ Prerequisites:
* [EagerWorkflowStart](src/EagerWorkflowStart) - Demonstrates usage of Eager Workflow Start to reduce latency for workflows that start with a local activity.
* [Encryption](src/Encryption) - End-to-end encryption with Temporal payload codecs.
* [EnvConfig](src/EnvConfig) - Load client configuration from TOML files with programmatic overrides
+* [Gcp/CloudRun](src/Gcp/CloudRun) - Run a Temporal Worker on a Google Cloud Run worker pool using the Cloud Run Id and OpenTelemetry plugins.
* [LambdaWorker](src/LambdaWorker) - Run a Temporal Worker inside an AWS Lambda function.
* [Mutex](src/Mutex) - How to implement a mutex as a workflow. Demonstrates how to avoid race conditions or parallel mutually exclusive operations on the same resource.
* [NexusCancellation](src/NexusCancellation) - Demonstrates how to cancel a running Nexus operation from a caller workflow.
diff --git a/TemporalioSamples.slnx b/TemporalioSamples.slnx
index 92d2dd4..955823b 100644
--- a/TemporalioSamples.slnx
+++ b/TemporalioSamples.slnx
@@ -47,6 +47,9 @@
+
+
+
diff --git a/src/Gcp/CloudRun/Dockerfile b/src/Gcp/CloudRun/Dockerfile
new file mode 100644
index 0000000..ede2c6b
--- /dev/null
+++ b/src/Gcp/CloudRun/Dockerfile
@@ -0,0 +1,14 @@
+# syntax=docker/dockerfile:1
+# Build from the repo root: docker build -f src/Gcp/CloudRun/Dockerfile -t .
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+COPY global.json Directory.Build.props Directory.Packages.props .editorconfig ./
+COPY src/Gcp/CloudRun/ ./src/Gcp/CloudRun/
+RUN dotnet publish src/Gcp/CloudRun/TemporalioSamples.Gcp.CloudRun.csproj -c Release -o /app
+
+FROM mcr.microsoft.com/dotnet/runtime:10.0
+RUN useradd --create-home --uid 10001 worker
+WORKDIR /app
+COPY --from=build --chown=worker:worker /app ./
+USER 10001
+ENTRYPOINT ["dotnet", "TemporalioSamples.Gcp.CloudRun.dll"]
diff --git a/src/Gcp/CloudRun/GreetingActivities.cs b/src/Gcp/CloudRun/GreetingActivities.cs
new file mode 100644
index 0000000..7c66e06
--- /dev/null
+++ b/src/Gcp/CloudRun/GreetingActivities.cs
@@ -0,0 +1,14 @@
+namespace TemporalioSamples.Gcp.CloudRun;
+
+using Microsoft.Extensions.Logging;
+using Temporalio.Activities;
+
+public static class GreetingActivities
+{
+ [Activity]
+ public static string SayHello(string name)
+ {
+ ActivityExecutionContext.Current.Logger.LogInformation("SayHello activity: {Name}", name);
+ return $"Hello, {name}!";
+ }
+}
diff --git a/src/Gcp/CloudRun/GreetingWorkflow.workflow.cs b/src/Gcp/CloudRun/GreetingWorkflow.workflow.cs
new file mode 100644
index 0000000..e3e8297
--- /dev/null
+++ b/src/Gcp/CloudRun/GreetingWorkflow.workflow.cs
@@ -0,0 +1,19 @@
+namespace TemporalioSamples.Gcp.CloudRun;
+
+using Microsoft.Extensions.Logging;
+using Temporalio.Workflows;
+
+[Workflow]
+public class GreetingWorkflow
+{
+ [WorkflowRun]
+ public async Task RunAsync(string name)
+ {
+ Workflow.Logger.LogInformation("GreetingWorkflow started: {Name}", name);
+ var result = await Workflow.ExecuteActivityAsync(
+ () => GreetingActivities.SayHello(name),
+ new() { StartToCloseTimeout = TimeSpan.FromSeconds(10) });
+ Workflow.Logger.LogInformation("GreetingWorkflow completed: {Result}", result);
+ return result;
+ }
+}
diff --git a/src/Gcp/CloudRun/Program.cs b/src/Gcp/CloudRun/Program.cs
new file mode 100644
index 0000000..62479c0
--- /dev/null
+++ b/src/Gcp/CloudRun/Program.cs
@@ -0,0 +1,73 @@
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging;
+using Temporalio.Client;
+using Temporalio.Common.EnvConfig;
+using Temporalio.Extensions.Gcp.CloudRun.Id;
+using Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry;
+using Temporalio.Worker;
+using TemporalioSamples.Gcp.CloudRun;
+
+// Connect from environment config (TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, TEMPORAL_API_KEY, ...).
+var connectOptions = ClientEnvConfig.LoadClientConnectOptions();
+connectOptions.TargetHost ??= "localhost:7233";
+connectOptions.LoggerFactory = LoggerFactory.Create(builder =>
+ builder.
+ AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] ").
+ SetMinimumLevel(LogLevel.Information));
+
+var taskQueue = Environment.GetEnvironmentVariable("TEMPORAL_TASK_QUEUE") ?? "cloud-run-worker";
+
+// @@@SNIPSTART dotnet-cloud-run
+// The Cloud Run Id plugin sets the client identity to "{instanceId}@{revision}" from Cloud Run
+// metadata at connect time; every Worker created from the client inherits it.
+connectOptions.Plugins = new ITemporalClientPlugin[] { new CloudRunIdPlugin() };
+
+// ApplyGoogleCloudRunOpenTelemetryDefaults adds the tracing interceptor and a runtime exporting Core
+// metrics + traces over OTLP to the collector sidecar; the returned handle owns the tracer provider.
+// Both plugins configure the same connect options.
+using var telemetry = connectOptions.ApplyGoogleCloudRunOpenTelemetryDefaults();
+
+var client = await TemporalClient.ConnectAsync(connectOptions);
+// @@@SNIPEND
+using var cts = new CancellationTokenSource();
+Console.CancelKeyPress += (_, eventArgs) =>
+{
+ eventArgs.Cancel = true;
+ cts.Cancel();
+};
+
+// Cloud Run sends SIGTERM ~10s before SIGKILL; defer the default termination so the worker can drain.
+using var sigterm = PosixSignalRegistration.Create(
+ PosixSignal.SIGTERM,
+ ctx =>
+ {
+ ctx.Cancel = true;
+ cts.Cancel();
+ });
+
+using var worker = new TemporalWorker(
+ client,
+ new TemporalWorkerOptions(taskQueue)
+ {
+ GracefulShutdownTimeout = TimeSpan.FromSeconds(5),
+ }.
+ AddWorkflow().
+ AddActivity(GreetingActivities.SayHello));
+
+Console.WriteLine(
+ "Worker running: taskQueue={0} address={1} namespace={2}",
+ taskQueue,
+ connectOptions.TargetHost,
+ connectOptions.Namespace ?? "default");
+try
+{
+ await worker.ExecuteAsync(cts.Token);
+}
+catch (OperationCanceledException)
+{
+ Console.WriteLine("Worker shutting down");
+}
+
+// Flush traces within the shutdown grace window (Core metrics export periodically, no explicit flush).
+await telemetry.FlushAsync(TimeSpan.FromSeconds(2));
+Console.WriteLine("Worker stopped");
diff --git a/src/Gcp/CloudRun/README.md b/src/Gcp/CloudRun/README.md
new file mode 100644
index 0000000..08249bd
--- /dev/null
+++ b/src/Gcp/CloudRun/README.md
@@ -0,0 +1,65 @@
+# Cloud Run Worker
+
+Run a Temporal Worker on a
+[Google Cloud Run worker pool](https://cloud.google.com/run/docs/deploy-worker-pools) using both GCP
+Cloud Run extensions together:
+
+- `Temporalio.Extensions.Gcp.CloudRun.Id` derives the client identity `{instanceId}@{revision}` from
+ Cloud Run instance metadata, so each instance is identifiable in Temporal.
+- `Temporalio.Extensions.Gcp.CloudRun.OpenTelemetry` exports Core SDK metrics and traces to a
+ [Google-Built OpenTelemetry Collector](https://cloud.google.com/stackdriver/docs/instrumentation/opentelemetry-collector-cloud-run)
+ sidecar (metrics to Managed Service for Prometheus, traces to Cloud Trace).
+
+`Program.cs` registers a `CloudRunIdPlugin` on `TemporalClientConnectOptions.Plugins` and calls
+`ApplyGoogleCloudRunOpenTelemetryDefaults()`, which adds the tracing interceptor and an OTLP exporter
+aimed at the sidecar, then runs a greeting workflow and activity until SIGTERM.
+
+## Prerequisites
+
+- A Temporal server the worker pool can reach (`TEMPORAL_ADDRESS` / `TEMPORAL_NAMESPACE`)
+- A Google Cloud project with the Cloud Run and Artifact Registry APIs enabled
+- [`gcloud`](https://cloud.google.com/sdk/docs/install), authenticated with the project set
+- The [Temporal CLI](https://docs.temporal.io/cli) and .NET 10
+
+## Deploy
+
+Set the placeholders used by `worker-pool.yaml`, then build the image, store the collector config as
+a secret, and deploy. Run from the repo root:
+
+```bash
+export REGION=us-central1 WORKER_POOL=temporal-cloud-run-worker INSTANCE_COUNT=1
+export SERVICE_ACCOUNT_EMAIL=@.iam.gserviceaccount.com
+export WORKER_IMAGE=$REGION-docker.pkg.dev/$(gcloud config get-value project)/samples/cloud-run
+export TEMPORAL_ADDRESS= TEMPORAL_NAMESPACE= TEMPORAL_TASK_QUEUE=cloud-run-worker
+export COLLECTOR_CONFIG_SECRET=otel-collector-config COLLECTOR_CONFIG_SECRET_VERSION=latest
+# Temporal Cloud: the API key the worker authenticates with, stored in Secret Manager.
+export TEMPORAL_API_KEY_SECRET=temporal-api-key TEMPORAL_API_KEY_SECRET_VERSION=latest
+
+# One-time setup: Artifact Registry repo, collector-config secret, and Temporal Cloud API key secret.
+gcloud artifacts repositories create samples --repository-format=docker --location "$REGION"
+gcloud secrets create "$COLLECTOR_CONFIG_SECRET" --data-file=src/Gcp/CloudRun/collector-config.yaml
+# Temporal Cloud: store the API key (from `tcld apikey create ...`) so the worker can read it.
+printf '%s' "" | gcloud secrets create "$TEMPORAL_API_KEY_SECRET" --data-file=-
+
+# Build, push, and deploy (re-run to update). Cloud Run requires linux/amd64 images, so build for
+# that platform explicitly (on arm64 Macs a native build would produce an unrunnable arm64 image).
+docker build --platform linux/amd64 -f src/Gcp/CloudRun/Dockerfile -t "$WORKER_IMAGE" . && docker push "$WORKER_IMAGE"
+envsubst < src/Gcp/CloudRun/worker-pool.yaml > /tmp/worker-pool.yaml
+gcloud run worker-pools replace /tmp/worker-pool.yaml
+```
+
+The service account needs the `monitoring.metricWriter`, `cloudtrace.agent`, and
+`secretmanager.secretAccessor` roles. The worker reads `TEMPORAL_API_KEY` from the Secret Manager
+secret above (see `worker-pool.yaml`) to authenticate to Temporal Cloud.
+
+## Run a workflow
+
+For Temporal Cloud, pass your connection values (`--address`, `--namespace`, `--api-key`):
+
+```bash
+temporal workflow execute --task-queue cloud-run-worker --type GreetingWorkflow --input '"Temporal"' \
+ --address "$TEMPORAL_ADDRESS" --namespace "$TEMPORAL_NAMESPACE" --api-key "$TEMPORAL_API_KEY" --tls
+```
+
+Metrics appear in Metrics Explorer and traces in Trace Explorer, tagged with the Cloud Run identity.
+Delete the pool with `gcloud run worker-pools delete "$WORKER_POOL" --region "$REGION"`.
diff --git a/src/Gcp/CloudRun/TemporalioSamples.Gcp.CloudRun.csproj b/src/Gcp/CloudRun/TemporalioSamples.Gcp.CloudRun.csproj
new file mode 100644
index 0000000..3e9c317
--- /dev/null
+++ b/src/Gcp/CloudRun/TemporalioSamples.Gcp.CloudRun.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+
+
+
+
+
+
+
+
diff --git a/src/Gcp/CloudRun/collector-config.yaml b/src/Gcp/CloudRun/collector-config.yaml
new file mode 100644
index 0000000..56b8837
--- /dev/null
+++ b/src/Gcp/CloudRun/collector-config.yaml
@@ -0,0 +1,84 @@
+# Google-Built OpenTelemetry Collector config for the sidecar: metrics -> Managed Service for Prometheus, traces -> Cloud Trace. Auth via the worker-pool service account (ADC).
+receivers:
+ # otelcol-google 0.156.0 has no `otlp_grpc` receiver (that name exists only as an exporter); `otlp`
+ # is the canonical OTLP receiver here.
+ otlp:
+ protocols:
+ grpc:
+ endpoint: localhost:4317
+
+processors:
+ # Batch traces for throughput. Do NOT add a batch processor to the cumulative-metrics pipeline: a
+ # shutdown flush could be batched with a recent periodic export of the same series and rejected as
+ # a duplicate time series.
+ batch/traces:
+ send_batch_max_size: 200
+ send_batch_size: 200
+ timeout: 5s
+ memory_limiter:
+ check_interval: 1s
+ limit_percentage: 65
+ spike_limit_percentage: 20
+ resource_detection:
+ detectors: [gcp]
+ timeout: 10s
+ # Rename Temporal datapoint labels that collide with the target labels Managed Service for Prometheus injects (e.g. `namespace`).
+ transform/collision:
+ metric_statements:
+ - context: datapoint
+ statements:
+ - set(attributes["exported_location"], attributes["location"])
+ - delete_key(attributes, "location")
+ - set(attributes["exported_cluster"], attributes["cluster"])
+ - delete_key(attributes, "cluster")
+ - set(attributes["exported_namespace"], attributes["namespace"])
+ - delete_key(attributes, "namespace")
+ - set(attributes["exported_job"], attributes["job"])
+ - delete_key(attributes, "job")
+ - set(attributes["exported_instance"], attributes["instance"])
+ - delete_key(attributes, "instance")
+ - set(attributes["exported_project_id"], attributes["project_id"])
+ - delete_key(attributes, "project_id")
+ # The Telemetry API expects the Google Cloud project in gcp.project_id.
+ transform/set_project_id:
+ error_mode: ignore
+ trace_statements:
+ - set(resource.attributes["gcp.project_id"], resource.attributes["gcp.project.id"]) where resource.attributes["gcp.project.id"] != nil
+ - set(resource.attributes["gcp.project_id"], resource.attributes["cloud.account.id"]) where resource.attributes["gcp.project_id"] == nil and resource.attributes["cloud.account.id"] != nil
+
+exporters:
+ googlemanagedprometheus:
+ otlp_grpc:
+ endpoint: telemetry.googleapis.com:443
+ compression: none
+ balancer_name: pick_first
+ auth:
+ authenticator: googleclientauth
+
+extensions:
+ health_check:
+ endpoint: 0.0.0.0:13133
+ googleclientauth:
+
+service:
+ extensions:
+ - health_check
+ - googleclientauth
+ pipelines:
+ metrics:
+ receivers: [otlp]
+ processors: [memory_limiter, resource_detection, transform/collision]
+ exporters: [googlemanagedprometheus]
+ traces:
+ receivers: [otlp]
+ processors: [memory_limiter, resource_detection, transform/set_project_id, batch/traces]
+ exporters: [otlp_grpc]
+ telemetry:
+ metrics:
+ readers:
+ - periodic:
+ exporter:
+ otlp:
+ protocol: grpc
+ endpoint: http://localhost:4317
+ insecure: true
diff --git a/src/Gcp/CloudRun/worker-pool.yaml b/src/Gcp/CloudRun/worker-pool.yaml
new file mode 100644
index 0000000..1bf1878
--- /dev/null
+++ b/src/Gcp/CloudRun/worker-pool.yaml
@@ -0,0 +1,62 @@
+# Cloud Run WorkerPool: a Temporal Worker + a Collector sidecar. Render placeholders with `envsubst` (see README) then `gcloud run worker-pools replace`. For Temporal Cloud, add TEMPORAL_API_KEY from a Secret Manager secretKeyRef.
+apiVersion: run.googleapis.com/v1
+kind: WorkerPool
+metadata:
+ name: "${WORKER_POOL}"
+ labels:
+ cloud.googleapis.com/location: "${REGION}"
+ annotations:
+ run.googleapis.com/scalingMode: manual
+ run.googleapis.com/manualInstanceCount: "${INSTANCE_COUNT}"
+spec:
+ template:
+ metadata:
+ annotations:
+ run.googleapis.com/container-dependencies: '{"worker":["collector"]}'
+ run.googleapis.com/execution-environment: gen2
+ spec:
+ containerConcurrency: 0
+ serviceAccountName: "${SERVICE_ACCOUNT_EMAIL}"
+ containers:
+ - name: worker
+ image: "${WORKER_IMAGE}"
+ env:
+ - name: TEMPORAL_ADDRESS
+ value: "${TEMPORAL_ADDRESS}"
+ - name: TEMPORAL_NAMESPACE
+ value: "${TEMPORAL_NAMESPACE}"
+ - name: TEMPORAL_TASK_QUEUE
+ value: "${TEMPORAL_TASK_QUEUE}"
+ # Temporal Cloud API key, read from Secret Manager (same pattern as the collector config).
+ - name: TEMPORAL_API_KEY
+ valueFrom:
+ secretKeyRef:
+ key: "${TEMPORAL_API_KEY_SECRET_VERSION}"
+ name: "${TEMPORAL_API_KEY_SECRET}"
+ - name: OTEL_EXPORTER_OTLP_ENDPOINT
+ value: http://localhost:4317
+ resources:
+ limits:
+ cpu: "1"
+ memory: 512Mi
+ - name: collector
+ image: us-docker.pkg.dev/cloud-ops-agents-artifacts/google-cloud-opentelemetry-collector/otelcol-google:0.156.0
+ args:
+ - --config=env:OTELCOL_CONFIG
+ env:
+ - name: OTELCOL_CONFIG
+ valueFrom:
+ secretKeyRef:
+ key: "${COLLECTOR_CONFIG_SECRET_VERSION}"
+ name: "${COLLECTOR_CONFIG_SECRET}"
+ startupProbe:
+ httpGet:
+ path: /
+ port: 13133
+ timeoutSeconds: 1
+ periodSeconds: 2
+ failureThreshold: 30
+ resources:
+ limits:
+ cpu: "1"
+ memory: 512Mi
diff --git a/tests/Gcp/CloudRun/GreetingWorkflowTests.cs b/tests/Gcp/CloudRun/GreetingWorkflowTests.cs
new file mode 100644
index 0000000..d32c54f
--- /dev/null
+++ b/tests/Gcp/CloudRun/GreetingWorkflowTests.cs
@@ -0,0 +1,34 @@
+namespace TemporalioSamples.Tests.Gcp.CloudRun;
+
+using Temporalio.Client;
+using Temporalio.Testing;
+using Temporalio.Worker;
+using TemporalioSamples.Gcp.CloudRun;
+using Xunit;
+using Xunit.Abstractions;
+
+public class GreetingWorkflowTests : TestBase
+{
+ public GreetingWorkflowTests(ITestOutputHelper output)
+ : base(output)
+ {
+ }
+
+ [TimeSkippingServerFact]
+ public async Task RunAsync_Greets()
+ {
+ await using var env = await WorkflowEnvironment.StartTimeSkippingAsync();
+ using var worker = new TemporalWorker(
+ env.Client,
+ new TemporalWorkerOptions("cloud-run-worker").
+ AddWorkflow().
+ AddActivity(GreetingActivities.SayHello));
+ await worker.ExecuteAsync(async () =>
+ {
+ var result = await env.Client.ExecuteWorkflowAsync(
+ (GreetingWorkflow wf) => wf.RunAsync("Temporal"),
+ new(id: $"workflow-{Guid.NewGuid()}", taskQueue: worker.Options.TaskQueue!));
+ Assert.Equal("Hello, Temporal!", result);
+ });
+ }
+}
diff --git a/tests/TemporalioSamples.Tests.csproj b/tests/TemporalioSamples.Tests.csproj
index 4ecadfd..c3f8b04 100644
--- a/tests/TemporalioSamples.Tests.csproj
+++ b/tests/TemporalioSamples.Tests.csproj
@@ -25,6 +25,7 @@
+