From 6cc1c4d4bebeb7bed1bae73746dd2babeb953be8 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Mon, 31 Aug 2026 15:04:54 -0700 Subject: [PATCH 01/16] Add .NET SDK support for Serverless Workers on AWS Lambda Documents the .NET SDK alongside Go, Python, TypeScript, and Java, verified against Temporalio.Extensions.Aws.Lambda 1.18.0 (public API read from the package's own XML documentation) and samples-dotnet@main src/LambdaWorker, which is maintained code with a test project. .NET specifics documented: - Separate NuGet package in lockstep with Temporalio 1.18.0, with OpenTelemetry in a second package rather than an extra on the first. - TemporalLambdaWorker.CreateHandler as the entry point, with sync and async configure overloads; registrations go through WorkerOptions. - Publish must be RID-specific (--runtime linux-x64 / linux-arm64): the SDK wraps a native Rust core, libtemporalio_sdk_core_c_bridge.so, which a portable publish omits. This is .NET's equivalent of Python's manylinux wheels and Go's GOARCH, with the same first-invocation failure mode. Includes the sample's presence check. - The handler string has three colon-separated parts, ASSEMBLY::NAMESPACE.TYPE::METHOD -- the only SDK with that shape. - Worker-level default versioning behavior is AutoUpgrade, where TypeScript's is PINNED. Defaults are not uniform; set them explicitly. - The SSL_CERT_FILE / root CA issue on some Lambda .NET images, which presents as a TLS failure that is not a configuration problem. - Telemetry IAM permissions and --tracing-config Mode=Active, from the sample's enable-telemetry.sh. Also corrects the sample location: the docs link to a branch (blob/ea/aws-lambda) that no longer exists; the sample is on main at src/LambdaWorker. Note that .NET uses --timeout 600 and --memory-size 256, the same as Go, Python and TypeScript, which supports reading Java's 90/1024 as a Java-specific choice rather than a documentation inconsistency. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 6 +- SKILL.md | 2 +- references/aws-lambda/diagnostics.md | 6 ++ references/aws-lambda/observability.md | 52 ++++++++++ references/aws-lambda/setup.md | 106 ++++++++++++++++++++- references/sdk-configuration.md | 127 ++++++++++++++++++++++--- 6 files changed, 283 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 2821dc6..116f067 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Deploy and operate [Temporal](https://temporal.io/) Workers on serverless comput ## What the skill can do -- Build Serverless Workers with the Go, Python, TypeScript, or Java SDK. +- Build Serverless Workers with the Go, Python, TypeScript, Java, or .NET SDK. - Package and deploy Workers to AWS Lambda with the correct architecture, timeout, and shutdown settings. - Configure the separate AWS roles used by the Lambda function and by Temporal. - Register a Worker Deployment Version, validate its Task Queue binding, and set it current. @@ -26,7 +26,7 @@ Deploy and operate [Temporal](https://temporal.io/) Workers on serverless comput |---|---| | Compute | AWS Lambda — Public Preview | | Temporal | Temporal Cloud and self-hosted Temporal Service | -| SDKs | Go, Python, TypeScript, Java | +| SDKs | Go, Python, TypeScript, Java, .NET | | Other compute providers | Not currently supported | For Temporal Cloud, the Namespace must be hosted on AWS. The Namespace and Lambda function may be in different AWS regions. @@ -120,7 +120,7 @@ Nothing is created before you approve the resource list. Troubleshooting and ins |---|---| | [`SKILL.md`](SKILL.md) | Core workflow, safety gates, provider rules, and reference routing | | [`references/concepts.md`](references/concepts.md) | Architecture, invocation flow, autoscaling, lifecycle, constraints, and use cases | -| [`references/sdk-configuration.md`](references/sdk-configuration.md) | Go, Python, TypeScript, and Java packages, entry points, versioning behavior, and tuned defaults | +| [`references/sdk-configuration.md`](references/sdk-configuration.md) | Go, Python, TypeScript, Java, and .NET packages, entry points, versioning behavior, and tuned defaults | | [`references/aws-lambda/setup.md`](references/aws-lambda/setup.md) | End-to-end deployment, verification, and teardown workflow | | [`references/aws-lambda/iam.md`](references/aws-lambda/iam.md) | Operator permissions, Lambda execution role, and Temporal invocation role | | [`references/aws-lambda/diagnostics.md`](references/aws-lambda/diagnostics.md) | Diagnostic decision tree and WCI inspection | diff --git a/SKILL.md b/SKILL.md index c6a96e6..0b71c3d 100644 --- a/SKILL.md +++ b/SKILL.md @@ -84,7 +84,7 @@ Where the harness has a todo list, use it *in addition to* the printed checklist **A step is complete when its verification passed — not when its command exited zero.** Several commands in this workflow exit clean having done nothing: the traffic-shifting and key-revocation commands no-op when their confirmation prompt goes unanswered, and providers return from create and update calls while the resource is still settling. Check an item off against state you read back, not against an exit code. When a step's verification fails, say which step you are on and what it is blocked on rather than moving down the list. -1. **Scope the task.** Identify the SDK language (Go, Python, TypeScript, or Java), the deployment target (Temporal Cloud or self-hosted — self-hosted has its own server prerequisites), the compute provider, and whether this is a new setup, a configuration change, or troubleshooting. Confirm the deployment target is compatible with the chosen provider — see "A Namespace on the target cloud provider is required" under Provider-neutral principles. Ensure a Temporal client/CLI is available and authenticated to the target. Each changes the specifics. → `references/concepts.md` for what the user is building; `references//setup.md` for the compatibility and client-setup details. +1. **Scope the task.** Identify the SDK language (Go, Python, TypeScript, Java, or .NET), the deployment target (Temporal Cloud or self-hosted — self-hosted has its own server prerequisites), the compute provider, and whether this is a new setup, a configuration change, or troubleshooting. Confirm the deployment target is compatible with the chosen provider — see "A Namespace on the target cloud provider is required" under Provider-neutral principles. Ensure a Temporal client/CLI is available and authenticated to the target. Each changes the specifics. → `references/concepts.md` for what the user is building; `references//setup.md` for the compatibility and client-setup details. **Put the compute provider in that batch of questions as a confirmable default, not a free choice.** Pre-select the supported provider from the table above and carry its support status in the option's description. The user confirms rather than chooses, so it costs no extra turn, but the provider is never something they were assumed into. Skip the question only when the request already names a provider. Do not restate any of this in a paragraph before the questions; the option description is where it belongs. diff --git a/references/aws-lambda/diagnostics.md b/references/aws-lambda/diagnostics.md index e5459d8..fdba30d 100644 --- a/references/aws-lambda/diagnostics.md +++ b/references/aws-lambda/diagnostics.md @@ -149,6 +149,12 @@ This is **not** a failure. It appears *after* Tasks have completed, is followed **Java — exec-format or `UnsupportedClassVersionError` at first invocation.** Bytecode targets a newer JDK than the runtime. Set `` (or the Gradle toolchain) to match `--runtime`. +**.NET — `DllNotFoundException` / missing `libtemporalio_sdk_core_c_bridge.so` at first invocation.** The .NET SDK wraps a native Rust core, and a portable (non-RID) publish omits its Linux build. Republish with an explicit runtime identifier matching the function's architecture (`--runtime linux-x64` for `x86_64`, `linux-arm64` for `arm64`) and check the file is in the publish output before zipping. → `setup.md` (.NET packaging). + +**.NET — TLS failure at first invocation despite correct address, Namespace and API key.** Some AWS Lambda .NET images override `SSL_CERT_FILE` in a way that prevents the SDK's Rust-based runtime from loading system root CAs. It looks like a connection or credential problem and is neither — the fix is the CA-loading workaround in the .NET SDK README, not changes to your Temporal configuration, IAM, or invocation role. Suspect it when the same credentials work from a local Worker against the same Namespace. + +**.NET — handler not found at first invocation.** The .NET handler string has **three** colon-separated parts, `ASSEMBLY::NAMESPACE.TYPE::METHOD`, and is the only SDK with that shape — Java uses two, the rest use `module.function`. Compare against the assembly name (not the project name, if they differ) and the fully-qualified type. + ### Check for Lambda timeout If the Lambda function reaches its configured timeout before the Worker finishes processing, AWS terminates the invocation. diff --git a/references/aws-lambda/observability.md b/references/aws-lambda/observability.md index 9b61c3f..ad4b42b 100644 --- a/references/aws-lambda/observability.md +++ b/references/aws-lambda/observability.md @@ -128,6 +128,56 @@ Attach the ADOT Collector layer. Because the OpenTelemetry SDK arrives as an ord --- +## .NET SDK + +### OTel package + +A **second NuGet package**, separate from the Lambda extension itself: + +```bash +dotnet add package Temporalio.Extensions.Aws.Lambda.OpenTelemetry +``` + +Published in lockstep with `Temporalio` and `Temporalio.Extensions.Aws.Lambda` (all 1.18.0). Unlike Python, where OTel is an extra on the existing package (`temporalio[lambda-worker-otel]`), .NET requires the extra reference. + +### OTel functions + +The package contributes an extension method on the options object, applied inside the configure callback: + +```csharp +TemporalLambdaWorker.CreateHandler( + new WorkerDeploymentVersion(deploymentName, buildId), + config => + { + config.ApplyOpenTelemetryDefaults(); + config.WorkerOptions.TaskQueue = taskQueue; + config.WorkerOptions.AddWorkflow().AddActivity(Activities.HelloActivity); + }); +``` + + +`ApplyOpenTelemetryDefaults()` configures metrics and tracing against the ADOT layer's collector. As with the other SDKs, telemetry must be exported before the invocation ends — keep any metrics export interval shorter than the Lambda timeout. + +### ADOT layer setup (.NET) + +Attach an **ADOT Collector layer** for the target region and architecture. No language-specific auto-instrumentation layer is needed, because the OpenTelemetry SDK arrives as an ordinary package dependency — the same situation as Go and Java. The sample's prerequisites list the collector layer ARN as something you supply per region. + +### Telemetry IAM permissions (.NET) + +The sample ships an `enable-telemetry.sh` that adds an inline policy to the **execution** role and turns on active tracing — a concrete, copyable form of the permissions listed under "Required IAM permissions" below: + +- `logs:CreateLogGroup`, `logs:CreateLogStream`, `logs:PutLogEvents`, scoped to `arn:aws:logs:::log-group:/aws/lambda/:*` +- `xray:PutTraceSegments`, `xray:PutTelemetryRecords` on `*` +- `cloudwatch:PutMetricData` on `*` + +It then runs `aws lambda update-function-configuration --tracing-config Mode=Active`, without which traces do not appear under the `AWS::Lambda::Function` filter in X-Ray. + +### Collector config env var (.NET) + +`OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/otel-collector-config.yaml`. The sample copies `otel-collector-config.yaml` into the publish directory before zipping so it lands in the task root. + +--- + ## TypeScript SDK ### OTel package @@ -259,6 +309,7 @@ For Python, the `AWSXRayDaemonWriteAccess` managed policy can be attached instea | Python | `OPENTELEMETRY_COLLECTOR_CONFIG_FILE` | | TypeScript | `OPENTELEMETRY_COLLECTOR_CONFIG_URI` | | Java | `OPENTELEMETRY_COLLECTOR_CONFIG_URI` | +| .NET | `OPENTELEMETRY_COLLECTOR_CONFIG_URI` (config file copied into the task root by the sample's deploy script) | ### ADOT layer summary @@ -268,6 +319,7 @@ For Python, the `AWSXRayDaemonWriteAccess` managed policy can be attached instea | Python | ADOT Python Lambda layer (includes collector and auto-instrumentation) | | TypeScript | ADOT JavaScript layer + ADOT Collector layer (`aws-otel-collector-amd64`) | | Java | ADOT Collector layer only (no language-specific layer; the OTel SDK is a Maven dependency of `temporal-aws-lambda`) | +| .NET | ADOT Collector layer only (no language-specific layer; OTel arrives via `Temporalio.Extensions.Aws.Lambda.OpenTelemetry`) | diff --git a/references/aws-lambda/setup.md b/references/aws-lambda/setup.md index 7624cdd..8f8f377 100644 --- a/references/aws-lambda/setup.md +++ b/references/aws-lambda/setup.md @@ -16,7 +16,7 @@ This is the end-to-end golden path: connect, write the Worker, package and deplo - Every Workflow must declare a versioning behavior, or the Worker must set a default versioning behavior. - An AWS account with permissions to create and invoke Lambda functions and create IAM roles. For the exact operator actions and a preflight check, see `iam.md`. - The AWS-specific steps require the `aws` CLI installed and configured with your AWS credentials. You may also use the AWS Console or the AWS SDKs. -- The Go SDK, Python SDK, TypeScript SDK, or Java SDK, depending on your language. +- The Go SDK, Python SDK, TypeScript SDK, Java SDK, or .NET SDK, depending on your language. - The `temporal` CLI, authenticated to the target Temporal Service — Steps 4–6 and the CLI troubleshooting paths use it. See "Temporal CLI and Cloud connection" below. Sample projects: @@ -24,6 +24,7 @@ Sample projects: - Python: [Python Lambda Worker sample](https://github.com/temporalio/samples-python/tree/main/lambda_worker) - TypeScript: [TypeScript Lambda Worker sample](https://github.com/temporalio/samples-typescript/tree/main/lambda-worker) - Java: [Java Lambda Worker sample](https://github.com/temporalio/samples-java/tree/main/lambda-worker) — three Gradle subprojects (`worker/` handler + greeting Workflow/Activity, `starter/` local client, `deploy/` IAM and deploy scripts plus a CloudFormation template) +- .NET: [.NET Lambda Worker sample](https://github.com/temporalio/samples-dotnet/tree/main/src/LambdaWorker) — `Worker/`, `Starter/`, and `Deploy/` (deploy, IAM-role, execution-role, and telemetry scripts plus a CloudFormation template), with a test project under `tests/LambdaWorker`. **The docs link to a branch (`blob/ea/aws-lambda`) that no longer exists; the sample is on `main` at `src/LambdaWorker`.** ## Temporal CLI and Cloud connection @@ -129,6 +130,7 @@ The Worker handles the per-invocation lifecycle: connecting to Temporal, polling | Python | `pip install temporalio` | `temporalio.contrib.aws.lambda_worker` ships inside the main `temporalio` package. Use `temporalio[lambda-worker-otel]` to add OpenTelemetry. | | TypeScript | `npm install @temporalio/lambda-worker` | Separate npm package from `@temporalio/worker`, versioned independently. | | Java | `io.temporal:temporal-aws-lambda` (Maven/Gradle) | **Separate artifact** from `io.temporal:temporal-sdk`, but on the **same version line** (both 1.38.0). Import `io.temporal:temporal-bom` in `dependencyManagement` to keep them aligned. `aws-lambda-java-core` 1.4.0 comes transitively. | +| .NET | `dotnet add package Temporalio.Extensions.Aws.Lambda` | **Separate NuGet package** from `Temporalio`, published in **lockstep** with it (both 1.18.0). OpenTelemetry is a *second* package, `Temporalio.Extensions.Aws.Lambda.OpenTelemetry`. Targets `netstandard2.0`; depends on `Amazon.Lambda.Core` 3.1.0. **Publish must be RID-specific** — the SDK carries a native Rust bridge. | ### Verify the installed API before generating code @@ -151,6 +153,13 @@ javap -cp ~/.m2/repository/io/temporal/temporal-aws-lambda//temporal-aws-la javap -cp 'io.temporal.aws.lambda.LambdaWorkerOptions$Builder' # or read the source directly — Maven Central publishes a sources jar: # curl -O https://repo1.maven.org/maven2/io/temporal/temporal-aws-lambda//temporal-aws-lambda--sources.jar + +# .NET — the .nupkg is a zip and ships full XML documentation for the public API +curl -sO https://api.nuget.org/v3-flatcontainer/temporalio.extensions.aws.lambda//temporalio.extensions.aws.lambda..nupkg +unzip -p temporalio.extensions.aws.lambda..nupkg \ + lib/netstandard2.0/Temporalio.Extensions.Aws.Lambda.xml +# the .nuspec lists the exact dependency versions: +unzip -p ...nupkg Temporalio.Extensions.Aws.Lambda.nuspec | grep dependency ``` **A useful ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. @@ -311,6 +320,46 @@ public class MyWorkflowImpl implements MyWorkflow { **Logging needs an SLF4J 1.7.x provider.** The SDK compiles against `slf4j-api:1.7.36`; a 2.x provider will not bind and the Worker produces no logs. Add `org.slf4j:slf4j-simple:1.7.36`. With it, the module logs `Temporal Lambda worker started … taskQueue=… identity=…` unprompted. → `sdk-configuration.md` (Java SDK). +### .NET + +Use the `Temporalio.Extensions.Aws.Lambda` package. A plain class exposes an async method that delegates to the handler returned by `TemporalLambdaWorker.CreateHandler`. + +```csharp +namespace MyCompany.Temporal.Worker; + +using Amazon.Lambda.Core; +using Temporalio.Common; +using Temporalio.Extensions.Aws.Lambda; + +public class LambdaFunction +{ + private static readonly Func WorkerHandler = + TemporalLambdaWorker.CreateHandler( + new WorkerDeploymentVersion("my-app", "build-1"), + config => + { + config.WorkerOptions.TaskQueue = "my-task-queue"; + config.WorkerOptions + .AddWorkflow() + .AddActivity(Activities.MyActivity); + }); + + public Task HandlerAsync(Stream input, ILambdaContext context) => + WorkerHandler(input, context); +} +``` + +Registrations go through `config.WorkerOptions`, an ordinary `TemporalWorkerOptions` — the same API a long-lived Worker uses. Use the `Func` overload when setup must await. + +Versioning behavior: the `[Workflow]` attribute, or a Worker-level default via `DefaultVersioningBehavior` in `DeploymentOptions`. + +```csharp +[Workflow(VersioningBehavior = VersioningBehavior.Pinned)] +public class MyWorkflow { ... } +``` + +**The .NET Worker-level default is `AutoUpgrade`** (TypeScript's is `PINNED`). Set it explicitly per Workflow rather than relying on either. → `sdk-configuration.md` (.NET SDK). + ## Step 2: Deploy Lambda function ### Build and package @@ -439,6 +488,38 @@ Excluding the signature files matters too: signed-jar signatures are invalid ins **Match the bytecode target to the runtime.** Compiling on a newer JDK than the function's runtime needs an explicit target — `17` for `--runtime java17`. This is the Java form of the architecture/wheel mismatch: it fails at invocation, not at build. +#### .NET + +Publish for an explicit Linux runtime identifier, then zip the publish output. + +```bash +dotnet publish path/to/Worker.csproj \ + --configuration Release \ + --runtime linux-x64 \ + --self-contained false \ + --output ./publish + +# Guard: the SDK's native Rust bridge must be in the output, or the function +# fails at FIRST INVOCATION, not at build time. +[[ -f ./publish/libtemporalio_sdk_core_c_bridge.so ]] || { + echo "Publish output is missing the linux-x64 Temporal native bridge." >&2; exit 1; } + +# If you use a temporal.toml / otel-collector-config.yaml, copy them in so they +# land in the Lambda task root: +cp temporal.toml otel-collector-config.yaml ./publish/ + +cd ./publish && zip -r ../function.zip . && cd .. +``` + +**The RID is not optional.** The .NET SDK wraps a native Rust core (`libtemporalio_sdk_core_c_bridge.so`); a portable publish omits the Linux build of it. This is .NET's equivalent of Python's `manylinux` wheels and Go's `GOARCH`, with the same late failure mode. Keep the RID consistent with `--architectures`: + +| `--runtime` | `--architectures` | +|---|---| +| `linux-x64` | `x86_64` | +| `linux-arm64` | `arm64` | + +`--self-contained false` is correct: the `dotnet8` managed runtime supplies the framework. + **Watch the artifact size — Java hits the 50 MB direct-upload ceiling early.** A hello-world Worker (one Workflow, one Activity, `slf4j-simple`) measured **41 MB**, versus ~14 MB for the equivalent Python package and 10–15 MB for Go. Anything with real dependencies will exceed 50 MB and must be uploaded via S3 (`--code S3Bucket=…,S3Key=…`) rather than `--zip-file fileb://`. Check before deploying: ```bash @@ -551,6 +632,29 @@ aws lambda create-function \ +#### .NET + + + +```bash +aws lambda create-function \ + --function-name my-temporal-worker \ + --runtime dotnet8 \ + --architectures x86_64 \ + --handler 'MyAssembly::MyCompany.Temporal.Worker.LambdaFunction::HandlerAsync' \ + --role \ + --zip-file fileb://function.zip \ + --timeout 600 \ + --memory-size 256 \ + --environment file:///tmp/lambda-env.json +``` + +- `--runtime`: `dotnet8` (the sample targets `net8.0`). +- `--handler`: **`ASSEMBLY::NAMESPACE.TYPE::METHOD` — three colon-separated parts**, and the only SDK with that shape. Java uses two (`Class::method`); Go, Python and TypeScript use `module.function`-style. Getting this wrong presents as a handler-not-found error at first invocation. +- `--timeout 600` / `--memory-size 256`: **the same values as Go, Python and TypeScript.** Only Java's example differs (90/1024), which supports reading that as a Java-specific choice rather than a documentation inconsistency. +- `--architectures` must match the publish RID (`linux-x64` → `x86_64`, `linux-arm64` → `arm64`). +- Temporal's deploy script retries `create-function` up to 12 times to absorb IAM propagation delay on a freshly created execution role — the same behavior described under "A freshly created execution role may not be assumable immediately" above. + ### Wait for the function to become Active `create-function` returns immediately with `"State": "Pending"`. The function cannot be invoked and `publish-version` fails while it is pending, so block on the state transition before the next step rather than sleeping a guessed interval: diff --git a/references/sdk-configuration.md b/references/sdk-configuration.md index 3ebe97b..578cee1 100644 --- a/references/sdk-configuration.md +++ b/references/sdk-configuration.md @@ -231,6 +231,107 @@ Resolution order (`LambdaWorkerOptions.resolveConfigFilePath`): --- +## .NET SDK + +### Package + +Import: `using Temporalio.Extensions.Aws.Lambda;` plus `Temporalio.Common` (for `WorkerDeploymentVersion`) and `Amazon.Lambda.Core` (for `ILambdaContext`). + +Install: `dotnet add package Temporalio.Extensions.Aws.Lambda` — a **separate NuGet package** from `Temporalio`, published in **lockstep** with it (both 1.18.0), the same relationship Java has. Published versions: 1.17.0 and 1.18.0. The package targets `netstandard2.0` and declares `Temporalio` 1.18.0 and `Amazon.Lambda.Core` 3.1.0. + +OpenTelemetry lives in a **second package**, `Temporalio.Extensions.Aws.Lambda.OpenTelemetry` (also 1.18.0) — unlike Python, where OTel is an extra on the same package. → `/observability.md`. + +### Entry point + +**`TemporalLambdaWorker.CreateHandler(version, configure)`** — returns a `Func` that your handler method delegates to. Overloads take either a synchronous `Action` or an asynchronous `Func` for setup that must await. A further overload takes `TemporalLambdaWorkerHandlerOptions`, which the XML docs describe as "internal test seams" — not for production use. + +```csharp +public class LambdaFunction +{ + private static readonly Func WorkerHandler = + TemporalLambdaWorker.CreateHandler( + new WorkerDeploymentVersion("my-app", "build-1"), + config => + { + config.WorkerOptions.TaskQueue = "my-task-queue"; + config.WorkerOptions + .AddWorkflow() + .AddActivity(Activities.MyActivity); + }); + + public Task HandlerAsync(Stream input, ILambdaContext context) => + WorkerHandler(input, context); +} +``` + +`TemporalLambdaWorker.LoadClientConnectOptions(...)` is also public, for loading connection options with Lambda-aware config resolution outside the handler. + +### Configure callback + +Receives a `TemporalLambdaWorkerOptions` with `ClientOptions`, `WorkerOptions`, `ShutdownDeadlineBuffer`, `ShutdownHooks`, and `AddShutdownHook(Func)`. The Task Queue and registrations go through `WorkerOptions` — an ordinary `TemporalWorkerOptions`, so `TaskQueue`, `AddWorkflow()` and `AddActivity(...)` behave exactly as they do for a long-lived Worker. The callback runs **per invocation** (Java is the outlier that runs its at cold start). + +### Versioning behavior + +Per-Workflow via the `[Workflow]` attribute: + +```csharp +[Workflow(VersioningBehavior = VersioningBehavior.Pinned)] +public class MyWorkflow { ... } +``` + +Or a Worker-level default through `DefaultVersioningBehavior` in `DeploymentOptions`. + +**The .NET Worker-level default is `AutoUpgrade`**, whereas TypeScript's is `PINNED`. Defaults are not uniform across SDKs — never state one globally, and prefer setting the behavior explicitly per Workflow. + +### Lambda-tuned defaults + + + +| Setting | Lambda default | +|---|---| +| `MaxConcurrentActivities` | 2 | +| `MaxConcurrentWorkflowTasks` | 10 | +| `MaxConcurrentLocalActivities` | 2 | +| `MaxConcurrentNexusTasks` | 5 | +| `MaxConcurrentWorkflowTaskPolls` | 2 | +| `MaxConcurrentActivityTaskPolls` | 1 | +| `MaxConcurrentNexusTaskPolls` | 1 | +| `MaxCachedWorkflows` | 30 | +| `GracefulShutdownTimeout` | 5 seconds | +| `ShutdownDeadlineBuffer` | 7 seconds | +| `DisableEagerActivityExecution` | Always `true`, cannot be overridden | + +### Native dependency — publish must be RID-specific + +The .NET SDK wraps a **native Rust core** (`libtemporalio_sdk_core_c_bridge.so`). A portable publish does not include the Linux build of it, and the failure appears only at first invocation. Always publish for an explicit runtime identifier matching the function's architecture: + +| `--runtime` | `--architectures` | +|---|---| +| `linux-x64` | `x86_64` | +| `linux-arm64` | `arm64` | + +This is .NET's equivalent of Python's `manylinux` wheels and Go's `GOARCH`. Temporal's own deploy script asserts the file is present before zipping, which is worth copying: + +```bash +[[ -f "$PUBLISH_DIR/libtemporalio_sdk_core_c_bridge.so" ]] || { + echo "Publish output is missing the $TARGET_RUNTIME Temporal native bridge." >&2; exit 1; } +``` + + +### Connection configuration + +Loaded automatically from environment variables and an optional TOML config file, with the same resolution order as the other SDKs: + +1. `TEMPORAL_CONFIG_FILE` environment variable, if set. +2. `temporal.toml` in the Lambda task root (typically `/var/task`). +3. `temporal.toml` in the current working directory. + +The sample copies a `temporal.toml` into the publish directory before zipping, so it lands in the task root, and keeps the API key in `TEMPORAL_API_KEY` rather than in the file. Supplying an API key enables TLS automatically. + +**TLS caveat specific to .NET:** some AWS Lambda .NET images override `SSL_CERT_FILE` in a way that prevents the SDK's Rust-based runtime from loading system root CAs. It surfaces as a TLS failure at first invocation. → `/diagnostics.md`. + +--- + ## TypeScript SDK ### Package @@ -310,20 +411,24 @@ The file is optional. If absent, only environment variables are used. From 9d84dba2fa93ab353f5613a5beb2c8e059020b37 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Mon, 31 Aug 2026 15:23:54 -0700 Subject: [PATCH 02/16] Fold findings from a real .NET deployment into the skill Deployed a .NET hello-world Worker end to end on Lambda. Three findings, none of which came from reading documentation. SSL_CERT_FILE is required, not optional. An otherwise-correct .NET deployment fails its first invocation with: Connection failed: Server connection error: tonic::transport::Error(Transport, NativeCertsNotFound) because AWS's .NET 8 Lambda images force-override SSL_CERT_FILE and the SDK's Rust core cannot load system root CAs. The variable now appears in the .NET create-function block and the environment-variable table rather than only in troubleshooting, since without it the deployment does not work at all. The error is also actively misleading: "certs not found" refers to the OS root CA store, not to any credential, and the connection fails before authentication is attempted. Diagnostics now says so explicitly and gives two discriminators, because the natural response -- checking the API key, Namespace, invocation role and External ID -- is wasted effort. Only .NET is affected: Python shares the Rust core but its runtime image does not override the variable, and Java uses the JVM truststore. Workflow.Logger is silent by default in .NET, because TemporalWorkerOptions.LoggerFactory defaults to the client's, which is also unset. Activity Console.WriteLine still reaches CloudWatch, so the gap looks selective rather than total. That completes a set of three SDKs with three unrelated causes for the same symptom. Also adds a caution to iam.md: read the invocation role's policy back after any update-stack that changes LambdaFunctionARNs. UPDATE_COMPLETE does not mean the ARNs are well-formed, the list is replaced rather than merged, and malformed entries silently revoke access for every function they omit. Includes the zsh $VAR:l expansion trap that produced exactly that outcome during this run. Co-Authored-By: Claude Opus 5 (1M context) --- references/aws-lambda/diagnostics.md | 18 +++++++++++++++++- references/aws-lambda/iam.md | 10 ++++++++++ references/aws-lambda/setup.md | 11 +++++++++++ references/sdk-configuration.md | 10 +++++++++- 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/references/aws-lambda/diagnostics.md b/references/aws-lambda/diagnostics.md index fdba30d..df1c47b 100644 --- a/references/aws-lambda/diagnostics.md +++ b/references/aws-lambda/diagnostics.md @@ -131,6 +131,9 @@ Common errors include: |---|---|---| | Python | `logging.basicConfig()` is a no-op when a root handler already exists, and the Lambda runtime installs one before your module is imported — so the level never changes and `INFO` records are filtered out | `logging.getLogger().setLevel(logging.INFO)` | | Java | The SDK compiles against `slf4j-api` **1.7.36**; a 2.x provider (`slf4j-simple:2.x`, Logback 1.3+) does not bind to a 1.7 API and nothing is emitted | use a 1.7.x provider, e.g. `org.slf4j:slf4j-simple:1.7.36` | +| .NET | `TemporalWorkerOptions.LoggerFactory` is unset and "defaults to the client logger factory", which is also unset — so `Workflow.Logger` output is discarded. Activity `Console.WriteLine` still reaches CloudWatch, which makes the gap look selective rather than total | set `config.WorkerOptions.LoggerFactory` (e.g. `LoggerFactory.Create(b => b.AddSimpleConsole().SetMinimumLevel(LogLevel.Information))`) | + +Three SDKs, three unrelated mechanisms, one symptom. Confirm invocation health from Lambda's runtime markers and metrics before concluding the Worker is broken — in every one of these cases it was not. **Java — `NullPointerException` in `ShutdownManager` on every invocation (benign).** As of `temporal-aws-lambda` 1.38.0, a normal graceful shutdown logs a `WARN` with a full stack trace: @@ -151,7 +154,20 @@ This is **not** a failure. It appears *after* Tasks have completed, is followed **.NET — `DllNotFoundException` / missing `libtemporalio_sdk_core_c_bridge.so` at first invocation.** The .NET SDK wraps a native Rust core, and a portable (non-RID) publish omits its Linux build. Republish with an explicit runtime identifier matching the function's architecture (`--runtime linux-x64` for `x86_64`, `linux-arm64` for `arm64`) and check the file is in the publish output before zipping. → `setup.md` (.NET packaging). -**.NET — TLS failure at first invocation despite correct address, Namespace and API key.** Some AWS Lambda .NET images override `SSL_CERT_FILE` in a way that prevents the SDK's Rust-based runtime from loading system root CAs. It looks like a connection or credential problem and is neither — the fix is the CA-loading workaround in the .NET SDK README, not changes to your Temporal configuration, IAM, or invocation role. Suspect it when the same credentials work from a local Worker against the same Namespace. +**.NET — `NativeCertsNotFound` at first invocation, despite a correct address, Namespace, and API key.** Observed verbatim on a real deployment: + +``` +System.InvalidOperationException: Connection failed: Server connection error: + tonic::transport::Error(Transport, NativeCertsNotFound) + at Temporalio.Bridge.Client.ConnectAsync(...) + at Temporalio.Client.TemporalConnection.ConnectAsync(...) +``` + +*Cause:* AWS's .NET 8 Lambda images force-override `SSL_CERT_FILE`, so the SDK's Rust core cannot load system root CAs. *Fix:* set `SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt` (or `/etc/ssl/certs/ca-certificates.crt`) on the function, then recover the binding as described under "Failed first invocation" — the failed validation invocation means no Task Queue was bound and Temporal will not retry on its own. + +**Do not read "certs not found" as a credentials problem.** It refers to the *operating system's root CA store*, not to any certificate of yours, and the failure happens before authentication is attempted. Chasing the API key, the Namespace, the invocation role, or the External ID is wasted effort. Two discriminators: the same credentials work from a local Worker against the same Namespace, and the stack trace terminates in `ConnectAsync` rather than in any Temporal API call. An API key auto-enables TLS, and TLS requires verifying the *server's* certificate chain — so supplying credentials is what creates the requirement, not what satisfies it. + +Only .NET is affected. Python uses the same Rust core (`temporalio/bridge/temporal_sdk_bridge.abi3.so`) but its runtime image does not override the variable; Java uses gRPC/Netty with the JVM truststore. **.NET — handler not found at first invocation.** The .NET handler string has **three** colon-separated parts, `ASSEMBLY::NAMESPACE.TYPE::METHOD`, and is the only SDK with that shape — Java uses two, the rest use `module.function`. Compare against the assembly name (not the project name, if they differ) and the fully-qualified type. diff --git a/references/aws-lambda/iam.md b/references/aws-lambda/iam.md index 0e58d2a..eadfb4e 100644 --- a/references/aws-lambda/iam.md +++ b/references/aws-lambda/iam.md @@ -210,6 +210,16 @@ Notes on the parameters above: EXTERNAL_ID=$(openssl rand -hex 16) ``` +**After any `update-stack` that changes `LambdaFunctionARNs`, read the resulting policy back — `UPDATE_COMPLETE` does not mean the ARNs are correct.** CloudFormation validates the template, not the semantics of a `CommaDelimitedList`, so malformed ARNs deploy cleanly and silently revoke access for every function whose ARN was mangled. The failure then looks like a Worker that stopped being invoked for no reason. + +```bash +aws iam get-role-policy --role-name \ + --policy-name Temporal-Cloud-Lambda-Invoke-Permissions \ + --query 'PolicyDocument.Statement[0].Resource' --output text | tr '\t' '\n' +``` + +Every entry must read `arn:aws:lambda:::function:` (and the `:*` form). **Build these ARNs from literal strings, not by concatenating a shell variable with a `:`-prefixed suffix.** In zsh, `$VAR:l` is the lowercase modifier: `"$PREFIX:lambda-my-worker"` silently expands to `` + `ambda-my-worker`, losing the `:l`, and produces exactly the kind of plausible-looking-but-wrong ARN this check catches. Remember the list is **replaced, not merged**, so a partial list revokes the functions it omits. + Retrieve the IAM role ARN from the stack outputs: ```bash diff --git a/references/aws-lambda/setup.md b/references/aws-lambda/setup.md index 8f8f377..8a39e24 100644 --- a/references/aws-lambda/setup.md +++ b/references/aws-lambda/setup.md @@ -649,6 +649,16 @@ aws lambda create-function \ --environment file:///tmp/lambda-env.json ``` +**The environment block for .NET must include `SSL_CERT_FILE`**, in addition to the usual `TEMPORAL_*` variables: + +```json +{"Variables":{ + "TEMPORAL_ADDRESS":"...", "TEMPORAL_NAMESPACE":"...", "TEMPORAL_API_KEY":"...", + "SSL_CERT_FILE":"/etc/pki/tls/certs/ca-bundle.crt"}} +``` + +Without it the **first invocation fails**, the Task Queue is never bound, and the Worker is never invoked again — an otherwise-correct deployment that simply does not work. AWS's .NET 8 Lambda images force-override `SSL_CERT_FILE`, which stops the SDK's Rust core from loading system root CAs. `/etc/ssl/certs/ca-certificates.crt` also works; try the other if one fails. This is server-certificate verification, unrelated to your API key. → `/diagnostics.md`. + - `--runtime`: `dotnet8` (the sample targets `net8.0`). - `--handler`: **`ASSEMBLY::NAMESPACE.TYPE::METHOD` — three colon-separated parts**, and the only SDK with that shape. Java uses two (`Class::method`); Go, Python and TypeScript use `module.function`-style. Getting this wrong presents as a handler-not-found error at first invocation. - `--timeout 600` / `--memory-size 256`: **the same values as Go, Python and TypeScript.** Only Java's example differs (90/1024), which supports reading that as a Java-specific choice rather than a documentation inconsistency. @@ -711,6 +721,7 @@ Java's recommended memory is 4× Python's, so each second of polling costs 4× a | `TEMPORAL_TLS_CLIENT_CERT_PATH` | Path to the TLS client certificate file for mTLS authentication. | | `TEMPORAL_TLS_CLIENT_KEY_PATH` | Path to the TLS client key file for mTLS authentication. | | `TEMPORAL_API_KEY` | API key for API key authentication. Supplying it auto-enables TLS; mTLS cert paths are not needed. | +| `SSL_CERT_FILE` | **Required for .NET on Lambda.** Path to the system CA bundle — `/etc/pki/tls/certs/ca-bundle.crt` or `/etc/ssl/certs/ca-certificates.crt`. AWS's .NET 8 images override this variable in a way that prevents the SDK's Rust core from loading root CAs, so TLS fails on the first invocation. Not needed for Go, Python, TypeScript, or Java. | The serverless Worker packages read environment variables and configuration files automatically at startup. For the full list of supported environment variables, config file format, and profiles, see the Environment configuration docs (`/develop/environment-configuration`). diff --git a/references/sdk-configuration.md b/references/sdk-configuration.md index 578cee1..d191b53 100644 --- a/references/sdk-configuration.md +++ b/references/sdk-configuration.md @@ -328,7 +328,15 @@ Loaded automatically from environment variables and an optional TOML config file The sample copies a `temporal.toml` into the publish directory before zipping, so it lands in the task root, and keeps the API key in `TEMPORAL_API_KEY` rather than in the file. Supplying an API key enables TLS automatically. -**TLS caveat specific to .NET:** some AWS Lambda .NET images override `SSL_CERT_FILE` in a way that prevents the SDK's Rust-based runtime from loading system root CAs. It surfaces as a TLS failure at first invocation. → `/diagnostics.md`. +**TLS caveat specific to .NET — set `SSL_CERT_FILE` or the first invocation fails.** AWS's .NET 8 Lambda images force-override `SSL_CERT_FILE`, which prevents the SDK's Rust core from loading system root CAs. Set it explicitly on the function: + +``` +SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt # or /etc/ssl/certs/ca-certificates.crt +``` + +**This is server-certificate verification, not client credentials.** The API key is unaffected and is not the problem — an API key auto-enables TLS, and TLS requires verifying Temporal Cloud's certificate chain against root CAs. The connection fails before authentication is ever attempted. + +Python shares the same Rust core (`temporalio/bridge/temporal_sdk_bridge.abi3.so`) but its Lambda image does not override the variable; Java uses gRPC/Netty and the JVM truststore, so neither is affected. → `/diagnostics.md`. --- From 54278c53f1e1d5ba2c756925f63d55547992121f Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Tue, 1 Sep 2026 12:28:14 -0700 Subject: [PATCH 03/16] minor updates to readme/sdk config --- README.md | 4 ++++ references/sdk-configuration.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index 116f067..670b2e6 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,10 @@ Add OpenTelemetry tracing to my Go Serverless Worker on Lambda. Package this Java Worker as a shaded jar and deploy it to Lambda. ``` +```text +Deploy this .NET Worker to Lambda with a runtime-specific publish. +``` + For a new deployment, the skill follows five stages: 1. **Scope** — confirm the SDK, compute provider, Namespace, region, and resource-naming prefix. diff --git a/references/sdk-configuration.md b/references/sdk-configuration.md index d191b53..1f4912c 100644 --- a/references/sdk-configuration.md +++ b/references/sdk-configuration.md @@ -440,5 +440,7 @@ Java-only: `MaxWorkflowThreadCount` = 30 (no counterpart elsewhere — Java runs + + Note: Go sticky cache size is 100, while Python and TypeScript are 30. These values come from each SDK's own docs and are not interchangeable. From ccdd389ce040411cbe9dd8b6a2e70910a9253032 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Tue, 1 Sep 2026 12:35:28 -0700 Subject: [PATCH 04/16] minor edits to fix unverifed reference to files --- references/aws-lambda/setup.md | 8 ++++---- references/sdk-configuration.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/references/aws-lambda/setup.md b/references/aws-lambda/setup.md index 8a39e24..dddaa69 100644 --- a/references/aws-lambda/setup.md +++ b/references/aws-lambda/setup.md @@ -23,7 +23,7 @@ Sample projects: - Go: [Go Lambda Worker sample](https://github.com/temporalio/samples-go/tree/main/lambda-worker) - Python: [Python Lambda Worker sample](https://github.com/temporalio/samples-python/tree/main/lambda_worker) - TypeScript: [TypeScript Lambda Worker sample](https://github.com/temporalio/samples-typescript/tree/main/lambda-worker) -- Java: [Java Lambda Worker sample](https://github.com/temporalio/samples-java/tree/main/lambda-worker) — three Gradle subprojects (`worker/` handler + greeting Workflow/Activity, `starter/` local client, `deploy/` IAM and deploy scripts plus a CloudFormation template) +- Java: [Java Lambda Worker sample](https://github.com/temporalio/samples-java/tree/main/lambda-worker) — three Gradle subprojects (`worker/` handler + greeting Workflow/Activity, `starter/` local client, `deploy/` IAM and deploy scripts plus a CloudFormation template) - .NET: [.NET Lambda Worker sample](https://github.com/temporalio/samples-dotnet/tree/main/src/LambdaWorker) — `Worker/`, `Starter/`, and `Deploy/` (deploy, IAM-role, execution-role, and telemetry scripts plus a CloudFormation template), with a test project under `tests/LambdaWorker`. **The docs link to a branch (`blob/ea/aws-lambda`) that no longer exists; the sample is on `main` at `src/LambdaWorker`.** ## Temporal CLI and Cloud connection @@ -624,11 +624,11 @@ aws lambda create-function \ --environment file:///tmp/lambda-env.json ``` -- `--runtime`: `java17` (or another supported Java version). -- `--handler`: `fully.qualified.Class::method` — **a different format from every other SDK**, which use `module.function` / `module.export`. Point it at the method that delegates to the `LambdaWorker.define` handler. +- `--runtime`: `java17` (or another supported Java version). +- `--handler`: `fully.qualified.Class::method` — **a different format from every other SDK**, which use `module.function` / `module.export`. Point it at the method that delegates to the `LambdaWorker.define` handler. - `--zip-file`: the shaded jar directly; no separate zip step. Switch to `--code S3Bucket=…,S3Key=…` once the jar exceeds 50 MB, which happens early in Java (see packaging above). - **`HOME=/tmp` is not needed** — unlike the Go and TypeScript examples. Verified: the Java module never reads `HOME`, and a missing config file is non-fatal. → `sdk-configuration.md` (Java SDK, Connection configuration). -- `--memory-size`: the docs recommend starting at `1024` because "Java Workers typically need more memory than other runtimes," then adjusting from CloudWatch. A measured hello-world used **240 MB of 1024** (`Max Memory Used` in the invocation's REPORT line), so `512` is usually ample for small Workers — and since Lambda bills GB-seconds, halving memory halves the bill. Start at 1024, read the metric, then cut. +- `--memory-size`: the docs recommend starting at `1024` because "Java Workers typically need more memory than other runtimes," then adjusting from CloudWatch. A measured hello-world used **240 MB of 1024** (`Max Memory Used` in the invocation's REPORT line), so `512` is usually ample for small Workers — and since Lambda bills GB-seconds, halving memory halves the bill. Start at 1024, read the metric, then cut. diff --git a/references/sdk-configuration.md b/references/sdk-configuration.md index 1f4912c..4fafa60 100644 --- a/references/sdk-configuration.md +++ b/references/sdk-configuration.md @@ -285,7 +285,7 @@ Or a Worker-level default through `DefaultVersioningBehavior` in `DeploymentOpti ### Lambda-tuned defaults - + | Setting | Lambda default | |---|---| From 55204c46899981ee88ebf43fcea6a214dea28bc4 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Tue, 1 Sep 2026 16:28:56 -0700 Subject: [PATCH 05/16] updates to some .net specifics --- references/aws-lambda/diagnostics.md | 4 ++-- references/aws-lambda/setup.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/references/aws-lambda/diagnostics.md b/references/aws-lambda/diagnostics.md index df1c47b..d459493 100644 --- a/references/aws-lambda/diagnostics.md +++ b/references/aws-lambda/diagnostics.md @@ -154,7 +154,7 @@ This is **not** a failure. It appears *after* Tasks have completed, is followed **.NET — `DllNotFoundException` / missing `libtemporalio_sdk_core_c_bridge.so` at first invocation.** The .NET SDK wraps a native Rust core, and a portable (non-RID) publish omits its Linux build. Republish with an explicit runtime identifier matching the function's architecture (`--runtime linux-x64` for `x86_64`, `linux-arm64` for `arm64`) and check the file is in the publish output before zipping. → `setup.md` (.NET packaging). -**.NET — `NativeCertsNotFound` at first invocation, despite a correct address, Namespace, and API key.** Observed verbatim on a real deployment: +**.NET — `NativeCertsNotFound` at first invocation, despite a correct address, Namespace, and API key.** ``` System.InvalidOperationException: Connection failed: Server connection error: @@ -165,7 +165,7 @@ System.InvalidOperationException: Connection failed: Server connection error: *Cause:* AWS's .NET 8 Lambda images force-override `SSL_CERT_FILE`, so the SDK's Rust core cannot load system root CAs. *Fix:* set `SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt` (or `/etc/ssl/certs/ca-certificates.crt`) on the function, then recover the binding as described under "Failed first invocation" — the failed validation invocation means no Task Queue was bound and Temporal will not retry on its own. -**Do not read "certs not found" as a credentials problem.** It refers to the *operating system's root CA store*, not to any certificate of yours, and the failure happens before authentication is attempted. Chasing the API key, the Namespace, the invocation role, or the External ID is wasted effort. Two discriminators: the same credentials work from a local Worker against the same Namespace, and the stack trace terminates in `ConnectAsync` rather than in any Temporal API call. An API key auto-enables TLS, and TLS requires verifying the *server's* certificate chain — so supplying credentials is what creates the requirement, not what satisfies it. +"Certs" here means the operating system's root CA store, not any credential of yours: an API key auto-enables TLS, and TLS requires verifying the *server's* certificate chain. The connection fails before authentication is attempted, so the API key, Namespace, invocation role, and External ID are all irrelevant. Two discriminators: the same credentials work from a local Worker against the same Namespace, and the stack trace ends in `ConnectAsync` rather than a Temporal API call. Only .NET is affected. Python uses the same Rust core (`temporalio/bridge/temporal_sdk_bridge.abi3.so`) but its runtime image does not override the variable; Java uses gRPC/Netty with the JVM truststore. diff --git a/references/aws-lambda/setup.md b/references/aws-lambda/setup.md index a8bf61f..a08c8a8 100644 --- a/references/aws-lambda/setup.md +++ b/references/aws-lambda/setup.md @@ -24,7 +24,7 @@ Sample projects: - Python: [Python Lambda Worker sample](https://github.com/temporalio/samples-python/tree/main/lambda_worker) - TypeScript: [TypeScript Lambda Worker sample](https://github.com/temporalio/samples-typescript/tree/main/lambda-worker) - Java: [Java Lambda Worker sample](https://github.com/temporalio/samples-java/tree/main/lambda-worker) — three Gradle subprojects (`worker/` handler + greeting Workflow/Activity, `starter/` local client, `deploy/` IAM and deploy scripts plus a CloudFormation template) -- .NET: [.NET Lambda Worker sample](https://github.com/temporalio/samples-dotnet/tree/main/src/LambdaWorker) — `Worker/`, `Starter/`, and `Deploy/` (deploy, IAM-role, execution-role, and telemetry scripts plus a CloudFormation template), with a test project under `tests/LambdaWorker`. **The docs link to a branch (`blob/ea/aws-lambda`) that no longer exists; the sample is on `main` at `src/LambdaWorker`.** +- .NET: [.NET Lambda Worker sample](https://github.com/temporalio/samples-dotnet/tree/main/src/LambdaWorker) — `Worker/`, `Starter/`, and `Deploy/` (deploy, IAM-role, execution-role, and telemetry scripts plus a CloudFormation template), with a test project under `tests/LambdaWorker`. ## Temporal CLI and Cloud connection @@ -511,7 +511,7 @@ cp temporal.toml otel-collector-config.yaml ./publish/ cd ./publish && zip -r ../function.zip . && cd .. ``` -**The RID is not optional.** The .NET SDK wraps a native Rust core (`libtemporalio_sdk_core_c_bridge.so`); a portable publish omits the Linux build of it. This is .NET's equivalent of Python's `manylinux` wheels and Go's `GOARCH`, with the same late failure mode. Keep the RID consistent with `--architectures`: +The .NET SDK wraps a native Rust core (`libtemporalio_sdk_core_c_bridge.so`). A portable publish omits the Linux build of it and the function fails at first invocation, not at build time — the same late failure mode as Python's `manylinux` wheels and Go's `GOARCH`. Keep the RID consistent with `--architectures`: | `--runtime` | `--architectures` | |---|---| From 7410d185e275402a1adfcd8f541a6265aa7251fa Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Tue, 1 Sep 2026 16:55:25 -0700 Subject: [PATCH 06/16] removing .NET related change --- references/aws-lambda/iam.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/references/aws-lambda/iam.md b/references/aws-lambda/iam.md index eadfb4e..0e58d2a 100644 --- a/references/aws-lambda/iam.md +++ b/references/aws-lambda/iam.md @@ -210,16 +210,6 @@ Notes on the parameters above: EXTERNAL_ID=$(openssl rand -hex 16) ``` -**After any `update-stack` that changes `LambdaFunctionARNs`, read the resulting policy back — `UPDATE_COMPLETE` does not mean the ARNs are correct.** CloudFormation validates the template, not the semantics of a `CommaDelimitedList`, so malformed ARNs deploy cleanly and silently revoke access for every function whose ARN was mangled. The failure then looks like a Worker that stopped being invoked for no reason. - -```bash -aws iam get-role-policy --role-name \ - --policy-name Temporal-Cloud-Lambda-Invoke-Permissions \ - --query 'PolicyDocument.Statement[0].Resource' --output text | tr '\t' '\n' -``` - -Every entry must read `arn:aws:lambda:::function:` (and the `:*` form). **Build these ARNs from literal strings, not by concatenating a shell variable with a `:`-prefixed suffix.** In zsh, `$VAR:l` is the lowercase modifier: `"$PREFIX:lambda-my-worker"` silently expands to `` + `ambda-my-worker`, losing the `:l`, and produces exactly the kind of plausible-looking-but-wrong ARN this check catches. Remember the list is **replaced, not merged**, so a partial list revokes the functions it omits. - Retrieve the IAM role ARN from the stack outputs: ```bash From 349fb7d476dad42fd2444dc9606742010f7425d1 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Tue, 1 Sep 2026 17:04:37 -0700 Subject: [PATCH 07/16] updates to code references again --- references/aws-lambda/observability.md | 6 +++--- references/aws-lambda/setup.md | 9 +++++++-- references/sdk-configuration.md | 8 ++++++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/references/aws-lambda/observability.md b/references/aws-lambda/observability.md index ad4b42b..b63864c 100644 --- a/references/aws-lambda/observability.md +++ b/references/aws-lambda/observability.md @@ -146,12 +146,12 @@ The package contributes an extension method on the options object, applied insid ```csharp TemporalLambdaWorker.CreateHandler( - new WorkerDeploymentVersion(deploymentName, buildId), + new WorkerDeploymentVersion("my-app", "build-1"), config => { config.ApplyOpenTelemetryDefaults(); - config.WorkerOptions.TaskQueue = taskQueue; - config.WorkerOptions.AddWorkflow().AddActivity(Activities.HelloActivity); + config.WorkerOptions.TaskQueue = "my-task-queue"; + config.WorkerOptions.AddWorkflow().AddActivity(MyActivities.MyActivity); }); ``` diff --git a/references/aws-lambda/setup.md b/references/aws-lambda/setup.md index a08c8a8..c6e2c9f 100644 --- a/references/aws-lambda/setup.md +++ b/references/aws-lambda/setup.md @@ -17,6 +17,7 @@ This is the end-to-end golden path: connect, write the Worker, package and deplo - An AWS account with permissions to create and invoke Lambda functions and create IAM roles. For the exact operator actions and a preflight check, see `iam.md`. - The AWS-specific steps require the `aws` CLI installed and configured with your AWS credentials. You may also use the AWS Console or the AWS SDKs. - The Go SDK, Python SDK, TypeScript SDK, Java SDK, or .NET SDK, depending on your language. +- The build toolchain for that SDK, because Step 2 packages the Worker with it: `go`, `pip`, `npm`, Maven or Gradle, or the `dotnet` CLI. Having the SDK as a dependency is not enough — confirm the toolchain is installed before starting. - The `temporal` CLI, authenticated to the target Temporal Service — Steps 4–6 and the CLI troubleshooting paths use it. See "Temporal CLI and Cloud connection" below. Sample projects: @@ -341,7 +342,7 @@ public class LambdaFunction config.WorkerOptions.TaskQueue = "my-task-queue"; config.WorkerOptions .AddWorkflow() - .AddActivity(Activities.MyActivity); + .AddActivity(MyActivities.MyActivity); }); public Task HandlerAsync(Stream input, ILambdaContext context) => @@ -355,7 +356,11 @@ Versioning behavior: the `[Workflow]` attribute, or a Worker-level default via ` ```csharp [Workflow(VersioningBehavior = VersioningBehavior.Pinned)] -public class MyWorkflow { ... } +public class MyWorkflow +{ + [WorkflowRun] + public async Task RunAsync(string name) => /* ... */; +} ``` **The .NET Worker-level default is `AutoUpgrade`** (TypeScript's is `PINNED`). Set it explicitly per Workflow rather than relying on either. → `sdk-configuration.md` (.NET SDK). diff --git a/references/sdk-configuration.md b/references/sdk-configuration.md index 4fafa60..0e6aa63 100644 --- a/references/sdk-configuration.md +++ b/references/sdk-configuration.md @@ -256,7 +256,7 @@ public class LambdaFunction config.WorkerOptions.TaskQueue = "my-task-queue"; config.WorkerOptions .AddWorkflow() - .AddActivity(Activities.MyActivity); + .AddActivity(MyActivities.MyActivity); }); public Task HandlerAsync(Stream input, ILambdaContext context) => @@ -276,7 +276,11 @@ Per-Workflow via the `[Workflow]` attribute: ```csharp [Workflow(VersioningBehavior = VersioningBehavior.Pinned)] -public class MyWorkflow { ... } +public class MyWorkflow +{ + [WorkflowRun] + public async Task RunAsync(string name) => /* ... */; +} ``` Or a Worker-level default through `DefaultVersioningBehavior` in `DeploymentOptions`. From 57f9317a38850b6f39886e5521a324dda129a8a2 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Tue, 1 Sep 2026 17:21:23 -0700 Subject: [PATCH 08/16] minor fix based on copilot suggestion --- references/aws-lambda/diagnostics.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/references/aws-lambda/diagnostics.md b/references/aws-lambda/diagnostics.md index d459493..63a5518 100644 --- a/references/aws-lambda/diagnostics.md +++ b/references/aws-lambda/diagnostics.md @@ -125,7 +125,7 @@ Common errors include: ### Language-specific signatures -**No application logs at all, but the Worker clearly ran.** Two different SDKs produce this same misleading silence by unrelated mechanisms, and in both cases the Worker is healthy — only the logging is broken. Diagnose invocation health from Lambda's own runtime markers (`INIT_START`/`START`/`END`/`REPORT`) and CloudWatch metrics instead, then fix the binding. +**No application logs at all, but the Worker clearly ran.** Three SDKs produce this same misleading silence by unrelated mechanisms, and in every case the Worker is healthy — only the logging is broken. Diagnose invocation health from Lambda's own runtime markers (`INIT_START`/`START`/`END`/`REPORT`) and CloudWatch metrics instead, then fix the binding. | SDK | Cause | Fix | |---|---|---| @@ -133,7 +133,7 @@ Common errors include: | Java | The SDK compiles against `slf4j-api` **1.7.36**; a 2.x provider (`slf4j-simple:2.x`, Logback 1.3+) does not bind to a 1.7 API and nothing is emitted | use a 1.7.x provider, e.g. `org.slf4j:slf4j-simple:1.7.36` | | .NET | `TemporalWorkerOptions.LoggerFactory` is unset and "defaults to the client logger factory", which is also unset — so `Workflow.Logger` output is discarded. Activity `Console.WriteLine` still reaches CloudWatch, which makes the gap look selective rather than total | set `config.WorkerOptions.LoggerFactory` (e.g. `LoggerFactory.Create(b => b.AddSimpleConsole().SetMinimumLevel(LogLevel.Information))`) | -Three SDKs, three unrelated mechanisms, one symptom. Confirm invocation health from Lambda's runtime markers and metrics before concluding the Worker is broken — in every one of these cases it was not. +Confirm invocation health from Lambda's runtime markers and metrics before concluding the Worker is broken — in every one of these cases it was not. **Java — `NullPointerException` in `ShutdownManager` on every invocation (benign).** As of `temporal-aws-lambda` 1.38.0, a normal graceful shutdown logs a `WARN` with a full stack trace: From a4c2be460bd1660a7425688e8a075290207580d2 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Wed, 2 Sep 2026 14:02:30 -0700 Subject: [PATCH 09/16] Carry dotnet-sdk-support's Java citation fixes into sdk-java.md The merged branch corrected four unverified doc citations in setup.md's Java create-function section. The SDK-reference refactor had moved that text to sdk-java.md, so resolving the merge in favour of the new structure dropped them. Apply them where the text now lives. Co-Authored-By: Claude Opus 5 (1M context) --- references/aws-lambda/sdk-java.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/references/aws-lambda/sdk-java.md b/references/aws-lambda/sdk-java.md index a1359c2..fc2807d 100644 --- a/references/aws-lambda/sdk-java.md +++ b/references/aws-lambda/sdk-java.md @@ -23,7 +23,7 @@ Install: `io.temporal:temporal-aws-lambda` — a **separate Maven artifact** fro `aws-lambda-java-core` (1.4.0) arrives transitively from `temporal-aws-lambda`; declare it explicitly if you compile against `RequestHandler`/`Context`. -- Java: [Java Lambda Worker sample](https://github.com/temporalio/samples-java/tree/main/lambda-worker) — three Gradle subprojects (`worker/` handler + greeting Workflow/Activity, `starter/` local client, `deploy/` IAM and deploy scripts plus a CloudFormation template) +- Java: [Java Lambda Worker sample](https://github.com/temporalio/samples-java/tree/main/lambda-worker) — three Gradle subprojects (`worker/` handler + greeting Workflow/Activity, `starter/` local client, `deploy/` IAM and deploy scripts plus a CloudFormation template) List the real public API of the resolved artifact before generating code: @@ -239,11 +239,11 @@ aws lambda create-function \ --environment file:///tmp/lambda-env.json ``` -- `--runtime`: `java17` (or another supported Java version). -- `--handler`: `fully.qualified.Class::method` — **a different format from every other SDK**, which use `module.function` / `module.export`. Point it at the method that delegates to the `LambdaWorker.define` handler. +- `--runtime`: `java17` (or another supported Java version). +- `--handler`: `fully.qualified.Class::method` — **a different format from every other SDK**, which use `module.function` / `module.export`. Point it at the method that delegates to the `LambdaWorker.define` handler. - `--zip-file`: the shaded jar directly; no separate zip step. Switch to `--code S3Bucket=…,S3Key=…` once the jar exceeds 50 MB, which happens early in Java (see packaging above). - **`HOME=/tmp` is not needed** — unlike the Go and TypeScript examples. Verified: the Java module never reads `HOME`, and a missing config file is non-fatal. → Connection configuration above. -- `--memory-size`: the docs recommend starting at `1024` because "Java Workers typically need more memory than other runtimes," then adjusting from CloudWatch. A measured hello-world used **240 MB of 1024** (`Max Memory Used` in the invocation's REPORT line), so `512` is usually ample for small Workers — and since Lambda bills GB-seconds, halving memory halves the bill. Start at 1024, read the metric, then cut. +- `--memory-size`: the docs recommend starting at `1024` because "Java Workers typically need more memory than other runtimes," then adjusting from CloudWatch. A measured hello-world used **240 MB of 1024** (`Max Memory Used` in the invocation's REPORT line), so `512` is usually ample for small Workers — and since Lambda bills GB-seconds, halving memory halves the bill. Start at 1024, read the metric, then cut. From 22144a0f5384ddf72e00b0025f81a2b99e0de0c2 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Wed, 2 Sep 2026 14:08:51 -0700 Subject: [PATCH 10/16] adding minor change to the top level skill md file --- SKILL.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/SKILL.md b/SKILL.md index 9c825a5..12406ba 100644 --- a/SKILL.md +++ b/SKILL.md @@ -51,7 +51,7 @@ Follow these steps in order. Each step is provider-neutral; the concrete command > - Nothing gets created before you approve that list. After approval the middle stretch runs unattended. > - At the end you get a Workflow you can watch execute, a full inventory of everything created, and an offer to remove it all. -**Write the summary provider-neutral, because at that point you do not know the provider.** It is one of the things step 1 asks. Say "your cloud account", never the name of a provider you have not been told. The same applies to the account, Namespace, and region: if a cheap read-only call has already told you (see step 1), name what you actually found; otherwise leave it out rather than filling it in with a plausible guess. +**Write the summary provider-neutral, because at that point you do not know the provider.** It is one of the things step 1 asks. Say "your cloud account", never the name of a provider you have not been told. The same applies to the account, Namespace, and region: leave out what you have not been told rather than filling it in with a plausible guess. The summary is the first thing in the run — it precedes the Namespace lookup, not the other way round. Skip the summary for troubleshooting, inspection, and configuration-change tasks. Someone whose Worker is not being invoked does not need an overview of a deployment they have already done. @@ -91,7 +91,9 @@ Where the harness has a todo list, use it *in addition to* the printed checklist **A step is complete when its verification passed — not when its command exited zero.** Several commands in this workflow exit clean having done nothing: the traffic-shifting and key-revocation commands no-op when their confirmation prompt goes unanswered, and providers return from create and update calls while the resource is still settling. Check an item off against state you read back, not against an exit code. When a step's verification fails, say which step you are on and what it is blocked on rather than moving down the list. -1. **Scope the task.** Identify the SDK language (Go, Python, TypeScript, Java, or .NET), the deployment target (Temporal Cloud or self-hosted — self-hosted has its own server prerequisites), the compute provider, and whether this is a new setup, a configuration change, or troubleshooting. Confirm the deployment target is compatible with the chosen provider — see "A Namespace on the target cloud provider is required" under Provider-neutral principles. Ensure a Temporal client/CLI is available and authenticated to the target. Each changes the specifics. → `references/concepts.md` for what the user is building; `references//setup.md` for the compatibility and client-setup details. +1. **Scope the task.** **Ask** — never infer — the SDK language (Go, Python, TypeScript, Java, or .NET), the deployment target (Temporal Cloud or self-hosted — self-hosted has its own server prerequisites), the compute provider, and whether this is a new setup, a configuration change, or troubleshooting. Confirm the deployment target is compatible with the chosen provider — see "A Namespace on the target cloud provider is required" under Provider-neutral principles. Each changes the specifics. → `references/concepts.md` for what the user is building; `references//setup.md` for the compatibility and client-setup details. + + **Nothing but the Namespace lookup runs before these questions.** The order is: summary, then `tcld namespace list` to populate the Namespace options, then one batch of questions. Do not probe the environment first — which CLIs are installed, what the working directory contains, which account a credential resolves to, which SDKs are on the machine. None of it answers what the user wants built, step 2 re-checks all of it properly, and running it first tells the user you have started work they have not scoped yet. A directory listing is never evidence of an SDK preference. **Put the compute provider in that batch of questions as a confirmable default, not a free choice.** Pre-select the supported provider from the table above and carry its support status in the option's description. The user confirms rather than chooses, so it costs no extra turn, but the provider is never something they were assumed into. Skip the question only when the request already names a provider. Do not restate any of this in a paragraph before the questions; the option description is where it belongs. @@ -103,9 +105,21 @@ Where the harness has a todo list, use it *in addition to* the printed checklist - **Summarize the ineligible ones in a single line** — "you also have 2 Namespaces on \, which this skill does not support" — rather than listing them individually or hiding them. A user who knows they have a Namespace and cannot find it in the list concludes the tool is broken; one line keeps them informed and explains the constraint. - **Name the account you are listing from and confirm it is the intended one** before showing anything. A stale credential lists a real account that is not the one the user means to deploy into, and every option under it looks authoritative. - **If more Namespaces are eligible than the question format can hold, print the labelled list and ask the user to name one.** Do not silently show only the first few. + - **Always include "create a new one" as an option**, even when eligible Namespaces exist. Standing up a first serverless Worker is exactly when a user wants a fresh Namespace, and a list with no way out makes them fight the question. Offer it last, and say it is a live, billable Temporal Cloud resource. This also settles the compute-provider answer, since a Namespace can only be served by compute on its own cloud provider — so a mismatch is caught here rather than at connection time, several steps later. + **Creating a Namespace, when that is what they picked.** Confirm the *spelling* before creating — names cannot be changed afterwards — and take the region from `tcld account list-regions`, never from memory: it returns `CloudProviderRegion` with a `CloudProvider` field, and entries whose provider is empty are not usable here. Most accounts can provision in only one region per provider, so there is often nothing to ask. + + ```bash + tcld namespace create -n --region --cloud-provider aws \ + --auth-method api_key --retention-days 30 + ``` + + Pass the **base name only** — `tcld` appends `.` itself, so `-n my-app` yields `my-app.a2dd6`. The call is asynchronous and returns `requestStatus.state: Pending`; poll `tcld namespace get -n ` until `state` is `Active` (tens of seconds) before using it, and read the frontend address from its `uri.grpc` rather than assembling one. Add the Namespace to the inventory in step 8 — it outlives the Worker and is not removed by the AWS teardown. + + **`tcld namespace list` starts a device-code login when the CLI is not authenticated** — it does not fail — so it can open a Temporal Cloud session the user never asked for. Say it may do that before you run it, and surface the verification URL if it does. + **Degrade gracefully if `tcld` is not authenticated.** Ask the user for the Namespace name rather than stopping to fix the login — they can copy it from the Cloud UI, where it appears on the Namespace page and in the URL. Ask for its region in the same batch of questions: the name alone does not tell you the provider, and a mismatch missed here surfaces at connection time instead. **Never source a Namespace, account, or resource identifier from shell history.** History is stale by construction — it is full of last quarter's accounts — and reading it to guess a deployment target produces confident, wrong answers. Take identifiers from the user or from an authenticated API call, and nowhere else. @@ -150,7 +164,7 @@ Where the harness has a todo list, use it *in addition to* the printed checklist 7. **Verify.** Start a Workflow on the Task Queue and confirm Temporal invokes the Worker — check the Workflow history in the Temporal UI and the compute provider's logs. If it does not progress, → `references//diagnostics.md`. -8. **Hand back the inventory first; offer teardown as the closing note.** The order is inventory → offer, never the reverse. Close with what now exists — compute unit and published build identifiers, roles, infrastructure stacks, region, deployment name and build ID — and what the run actually did, including anything you worked around or deviated from. Say plainly that it is live and billable. These names are only knowable from the run that created them, and reconstructing them later means scanning the user's account. +8. **Hand back the inventory first; offer teardown as the closing note.** The order is inventory → offer, never the reverse. Close with what now exists — compute unit and published build identifiers, roles, infrastructure stacks, region, deployment name and build ID, plus any Namespace or API key created during the run — and what the run actually did, including anything you worked around or deviated from. Say plainly that it is live and billable. These names are only knowable from the run that created them, and reconstructing them later means scanning the user's account. **Do not write a teardown script before the user asks for one.** Generating it unprompted buries the inventory under a file they did not request, and the inventory is what they need in order to decide. End with a single line — *"Let me know if you want a teardown script to remove these resources"* — and stop there. Write the script, or run the teardown, when they take you up on it. → `references//setup.md` (Teardown). @@ -239,5 +253,5 @@ Most questions need 2–3 reference files. - **General SDK development patterns** (Workflows, Activities, signals, queries, Worker Versioning concepts): see `skill-temporal-developer`. - **Traditional Worker tuning** (slot suppliers, tuners, poller autoscaling, resource-based tuning): see `skill-temporal-workertuning`. -- **Temporal Cloud administration** (Namespaces, users, certificates, billing): see `skill-temporal-ops`. +- **Temporal Cloud administration** (users, certificates, billing, and Namespace management generally): see `skill-temporal-ops`. The one exception is creating a Namespace to deploy into, which step 1 handles inline — a user standing up their first serverless Worker should not be sent to another skill mid-run. - **CLI command reference** (beyond the serverless-specific flags): see `skill-temporal-cli`. From 50735d827b29b64f39f73d8153ee55a87635340b Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Wed, 2 Sep 2026 14:54:02 -0700 Subject: [PATCH 11/16] Drops prose that argues for rules the bold leads already state. No rule or technical claim changes. - SKILL.md: remove trailing rationale from four rules; correct the pre-question note to point at steps 2 and 3, not step 2 alone. - sdk-*.md: drop "useful" from the "Ordering when sources disagree" heading, which the refactor had copied into all five files. --- SKILL.md | 8 ++++---- references/aws-lambda/sdk-dotnet.md | 2 +- references/aws-lambda/sdk-go.md | 2 +- references/aws-lambda/sdk-java.md | 2 +- references/aws-lambda/sdk-python.md | 2 +- references/aws-lambda/sdk-typescript.md | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/SKILL.md b/SKILL.md index 12406ba..8384c69 100644 --- a/SKILL.md +++ b/SKILL.md @@ -51,7 +51,7 @@ Follow these steps in order. Each step is provider-neutral; the concrete command > - Nothing gets created before you approve that list. After approval the middle stretch runs unattended. > - At the end you get a Workflow you can watch execute, a full inventory of everything created, and an offer to remove it all. -**Write the summary provider-neutral, because at that point you do not know the provider.** It is one of the things step 1 asks. Say "your cloud account", never the name of a provider you have not been told. The same applies to the account, Namespace, and region: leave out what you have not been told rather than filling it in with a plausible guess. The summary is the first thing in the run — it precedes the Namespace lookup, not the other way round. +**Write the summary provider-neutral, because at that point you do not know the provider.** Say "your cloud account", never the name of a provider you have not been told. The same applies to the account, Namespace, and region: leave out what you have not been told rather than filling it in with a plausible guess. Skip the summary for troubleshooting, inspection, and configuration-change tasks. Someone whose Worker is not being invoked does not need an overview of a deployment they have already done. @@ -93,7 +93,7 @@ Where the harness has a todo list, use it *in addition to* the printed checklist 1. **Scope the task.** **Ask** — never infer — the SDK language (Go, Python, TypeScript, Java, or .NET), the deployment target (Temporal Cloud or self-hosted — self-hosted has its own server prerequisites), the compute provider, and whether this is a new setup, a configuration change, or troubleshooting. Confirm the deployment target is compatible with the chosen provider — see "A Namespace on the target cloud provider is required" under Provider-neutral principles. Each changes the specifics. → `references/concepts.md` for what the user is building; `references//setup.md` for the compatibility and client-setup details. - **Nothing but the Namespace lookup runs before these questions.** The order is: summary, then `tcld namespace list` to populate the Namespace options, then one batch of questions. Do not probe the environment first — which CLIs are installed, what the working directory contains, which account a credential resolves to, which SDKs are on the machine. None of it answers what the user wants built, step 2 re-checks all of it properly, and running it first tells the user you have started work they have not scoped yet. A directory listing is never evidence of an SDK preference. + **Nothing but the Namespace lookup runs before these questions.** The order is: summary, then `tcld namespace list` to populate the Namespace options, then one batch of questions. Do not probe the environment first — which CLIs are installed, what the working directory contains, which account a credential resolves to, which SDKs are on the machine; steps 2 and 3 re-check what matters. A directory listing is never evidence of an SDK preference. **Put the compute provider in that batch of questions as a confirmable default, not a free choice.** Pre-select the supported provider from the table above and carry its support status in the option's description. The user confirms rather than chooses, so it costs no extra turn, but the provider is never something they were assumed into. Skip the question only when the request already names a provider. Do not restate any of this in a paragraph before the questions; the option description is where it belongs. @@ -105,7 +105,7 @@ Where the harness has a todo list, use it *in addition to* the printed checklist - **Summarize the ineligible ones in a single line** — "you also have 2 Namespaces on \, which this skill does not support" — rather than listing them individually or hiding them. A user who knows they have a Namespace and cannot find it in the list concludes the tool is broken; one line keeps them informed and explains the constraint. - **Name the account you are listing from and confirm it is the intended one** before showing anything. A stale credential lists a real account that is not the one the user means to deploy into, and every option under it looks authoritative. - **If more Namespaces are eligible than the question format can hold, print the labelled list and ask the user to name one.** Do not silently show only the first few. - - **Always include "create a new one" as an option**, even when eligible Namespaces exist. Standing up a first serverless Worker is exactly when a user wants a fresh Namespace, and a list with no way out makes them fight the question. Offer it last, and say it is a live, billable Temporal Cloud resource. + - **Always include "create a new one" as an option**, even when eligible Namespaces exist. Offer it last, and say it is a live, billable Temporal Cloud resource. This also settles the compute-provider answer, since a Namespace can only be served by compute on its own cloud provider — so a mismatch is caught here rather than at connection time, several steps later. @@ -164,7 +164,7 @@ Where the harness has a todo list, use it *in addition to* the printed checklist 7. **Verify.** Start a Workflow on the Task Queue and confirm Temporal invokes the Worker — check the Workflow history in the Temporal UI and the compute provider's logs. If it does not progress, → `references//diagnostics.md`. -8. **Hand back the inventory first; offer teardown as the closing note.** The order is inventory → offer, never the reverse. Close with what now exists — compute unit and published build identifiers, roles, infrastructure stacks, region, deployment name and build ID, plus any Namespace or API key created during the run — and what the run actually did, including anything you worked around or deviated from. Say plainly that it is live and billable. These names are only knowable from the run that created them, and reconstructing them later means scanning the user's account. +8. **Hand back the inventory first; offer teardown as the closing note.** Close with what now exists — compute unit and published build identifiers, roles, infrastructure stacks, region, deployment name and build ID, plus any Namespace or API key created during the run — and what the run actually did, including anything you worked around or deviated from. Say plainly that it is live and billable. **Do not write a teardown script before the user asks for one.** Generating it unprompted buries the inventory under a file they did not request, and the inventory is what they need in order to decide. End with a single line — *"Let me know if you want a teardown script to remove these resources"* — and stop there. Write the script, or run the teardown, when they take you up on it. → `references//setup.md` (Teardown). diff --git a/references/aws-lambda/sdk-dotnet.md b/references/aws-lambda/sdk-dotnet.md index d738450..2950aad 100644 --- a/references/aws-lambda/sdk-dotnet.md +++ b/references/aws-lambda/sdk-dotnet.md @@ -24,7 +24,7 @@ unzip -p temporalio.extensions.aws.lambda..nupkg \ unzip -p ...nupkg Temporalio.Extensions.Aws.Lambda.nuspec | grep dependency ``` -**A useful ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs, so check rather than pattern-match from another language. +**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs, so check rather than pattern-match from another language. ## Entry point diff --git a/references/aws-lambda/sdk-go.md b/references/aws-lambda/sdk-go.md index 8a2216e..1c3c367 100644 --- a/references/aws-lambda/sdk-go.md +++ b/references/aws-lambda/sdk-go.md @@ -19,7 +19,7 @@ go doc go.temporal.io/sdk/contrib/aws/lambdaworker go doc go.temporal.io/sdk/contrib/aws/lambdaworker.Options ``` -**A useful ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. +**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. ## Entry point diff --git a/references/aws-lambda/sdk-java.md b/references/aws-lambda/sdk-java.md index fc2807d..87a8eb2 100644 --- a/references/aws-lambda/sdk-java.md +++ b/references/aws-lambda/sdk-java.md @@ -35,7 +35,7 @@ javap -cp 'io.temporal.aws.lambda.LambdaWorkerOptions$Builder' # curl -O https://repo1.maven.org/maven2/io/temporal/temporal-aws-lambda//temporal-aws-lambda--sources.jar ``` -**A useful ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. +**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. ## Entry point diff --git a/references/aws-lambda/sdk-python.md b/references/aws-lambda/sdk-python.md index 583b1d2..cc07f24 100644 --- a/references/aws-lambda/sdk-python.md +++ b/references/aws-lambda/sdk-python.md @@ -18,7 +18,7 @@ Read the installed API before generating code: python -c "import temporalio.contrib.aws.lambda_worker as m; help(m.LambdaWorkerConfig)" ``` -**A useful ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. +**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. **Fastest path:** start from the language sample linked above — it has a working Worker, Workflow, and Activity already wired together. The handler example below imports the Workflow and Activity from separate modules (`my_workflows`, `my_activities`). When writing from scratch, create those modules with at least one registered Workflow (declaring a versioning behavior) and one Activity, and name the entry-point file to match the `--handler` you deploy (for example, `lambda_function.py` → `--handler lambda_function.lambda_handler`). diff --git a/references/aws-lambda/sdk-typescript.md b/references/aws-lambda/sdk-typescript.md index 603b1bb..9d98258 100644 --- a/references/aws-lambda/sdk-typescript.md +++ b/references/aws-lambda/sdk-typescript.md @@ -18,7 +18,7 @@ Check the installed version, then read its type declarations: npm ls @temporalio/lambda-worker ``` -**A useful ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. +**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. ## Entry point From f259b3a88d00631b979da17e9acd3d1ada07974a Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Thu, 3 Sep 2026 11:27:55 -0700 Subject: [PATCH 12/16] remove redundant/repeated information --- references/aws-lambda/sdk-dotnet.md | 14 +++++++------- references/aws-lambda/sdk-go.md | 2 +- references/aws-lambda/sdk-java.md | 22 ++++++++++------------ references/aws-lambda/sdk-python.md | 2 +- references/aws-lambda/sdk-typescript.md | 2 +- 5 files changed, 20 insertions(+), 22 deletions(-) diff --git a/references/aws-lambda/sdk-dotnet.md b/references/aws-lambda/sdk-dotnet.md index 2950aad..09e47eb 100644 --- a/references/aws-lambda/sdk-dotnet.md +++ b/references/aws-lambda/sdk-dotnet.md @@ -113,7 +113,7 @@ config.WorkerOptions.LoggerFactory = ## Connection configuration -Loaded automatically from environment variables and an optional TOML config file, with the same resolution order as the other SDKs: +Loaded automatically from environment variables and an optional TOML config file, in this resolution order: 1. `TEMPORAL_CONFIG_FILE` environment variable, if set. 2. `temporal.toml` in the Lambda task root (typically `/var/task`). @@ -129,7 +129,7 @@ SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt # or /etc/ssl/certs/ca-certif **This is server-certificate verification, not client credentials.** The API key is unaffected and is not the problem — an API key auto-enables TLS, and TLS requires verifying Temporal Cloud's certificate chain against root CAs. The connection fails before authentication is ever attempted. -Python shares the same Rust core (`temporalio/bridge/temporal_sdk_bridge.abi3.so`) but its Lambda image does not override the variable; Java uses gRPC/Netty and the JVM truststore, so neither is affected. → `diagnostics.md`. +No other SDK on Lambda needs this. → `diagnostics.md` for why. ## Build and package @@ -195,8 +195,8 @@ aws lambda create-function \ Without it the **first invocation fails**, the Task Queue is never bound, and the Worker is never invoked again — an otherwise-correct deployment that simply does not work. AWS's .NET 8 Lambda images force-override `SSL_CERT_FILE`, which stops the SDK's Rust core from loading system root CAs. `/etc/ssl/certs/ca-certificates.crt` also works; try the other if one fails. This is server-certificate verification, unrelated to your API key. → `diagnostics.md`. - `--runtime`: `dotnet8` (the sample targets `net8.0`). -- `--handler`: **`ASSEMBLY::NAMESPACE.TYPE::METHOD` — three colon-separated parts**, and the only SDK with that shape. Java uses two (`Class::method`); Go, Python and TypeScript use `module.function`-style. Getting this wrong presents as a handler-not-found error at first invocation. -- `--timeout 600` / `--memory-size 256`: **the same values as Go, Python and TypeScript.** Only Java's example differs (90/1024), which supports reading that as a Java-specific choice rather than a documentation inconsistency. +- `--handler`: **`ASSEMBLY::NAMESPACE.TYPE::METHOD` — three colon-separated parts.** Getting this wrong presents as a handler-not-found error at first invocation. +- `--timeout 600` / `--memory-size 256`. → `setup.md` for how to choose these; the timeout only has to clear startup, and memory is what drives cost. - `--architectures` must match the publish RID (`linux-x64` → `x86_64`, `linux-arm64` → `arm64`). - Temporal's deploy script retries `create-function` up to 12 times to absorb IAM propagation delay on a freshly created execution role — the same behavior described under "A freshly created execution role may not be assumable immediately" in `setup.md`. @@ -210,7 +210,7 @@ A **second NuGet package**, separate from the Lambda extension itself: -`ApplyOpenTelemetryDefaults()` configures metrics and tracing against the ADOT layer's collector. As with the other SDKs, telemetry must be exported before the invocation ends — keep any metrics export interval shorter than the Lambda timeout. +`ApplyOpenTelemetryDefaults()` configures metrics and tracing against the ADOT layer's collector. Telemetry must be exported before the invocation ends — keep any metrics export interval shorter than the Lambda timeout. ### ADOT layer setup @@ -271,4 +271,4 @@ System.InvalidOperationException: Connection failed: Server connection error: Only .NET is affected. Python uses the same Rust core (`temporalio/bridge/temporal_sdk_bridge.abi3.so`) but its runtime image does not override the variable; Java uses gRPC/Netty with the JVM truststore. -**.NET — handler not found at first invocation.** The .NET handler string has **three** colon-separated parts, `ASSEMBLY::NAMESPACE.TYPE::METHOD`, and is the only SDK with that shape — Java uses two, the rest use `module.function`. Compare against the assembly name (not the project name, if they differ) and the fully-qualified type. +**.NET — handler not found at first invocation.** The handler string has **three** colon-separated parts, `ASSEMBLY::NAMESPACE.TYPE::METHOD`. Compare against the assembly name (not the project name, if they differ) and the fully-qualified type. diff --git a/references/aws-lambda/sdk-go.md b/references/aws-lambda/sdk-go.md index 1c3c367..0b57603 100644 --- a/references/aws-lambda/sdk-go.md +++ b/references/aws-lambda/sdk-go.md @@ -19,7 +19,7 @@ go doc go.temporal.io/sdk/contrib/aws/lambdaworker go doc go.temporal.io/sdk/contrib/aws/lambdaworker.Options ``` -**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. +**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs, so check rather than pattern-match from another language. ## Entry point diff --git a/references/aws-lambda/sdk-java.md b/references/aws-lambda/sdk-java.md index 87a8eb2..cf6401f 100644 --- a/references/aws-lambda/sdk-java.md +++ b/references/aws-lambda/sdk-java.md @@ -35,15 +35,13 @@ javap -cp 'io.temporal.aws.lambda.LambdaWorkerOptions$Builder' # curl -O https://repo1.maven.org/maven2/io/temporal/temporal-aws-lambda//temporal-aws-lambda--sources.jar ``` -**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. +**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs, so check rather than pattern-match from another language. ## Entry point -**`LambdaWorker.define(version, configure)`** — returns a `RequestHandler` that your handler class delegates to. There are four public overloads: `define` (2- and 3-arg) and `newHandler` (2- and 3-arg, taking a pre-built `LambdaWorkerOptions`). +**`LambdaWorker.define(version, configure)`** — returns a `RequestHandler` that your handler class delegates to. There are four public overloads: `define` (2- and 3-arg) and `newHandler` (2- and 3-arg, taking a pre-built `LambdaWorkerOptions`). -Note that Java's entry point is not "run"-shaped like the other SDKs' (`RunWorker`, `run_worker`, `runWorker`) — confirm the method name against the version you install. - -## Configure callback — two phases, unlike the other SDKs +## Configure callback — two phases Java splits configuration in a way no other SDK does, and the distinction matters: @@ -113,7 +111,7 @@ public final class LambdaFunction implements RequestHandler { } ``` -The entry point is `define` (or `newHandler` for pre-built options) — not a "run"-shaped name like the other SDKs use. Temporal's [sample handler](https://github.com/temporalio/samples-java/blob/main/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java) is the reference implementation. +The entry point is `define` (or `newHandler` for pre-built options). Temporal's [sample handler](https://github.com/temporalio/samples-java/blob/main/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java) is the reference implementation. ## Lambda-tuned defaults @@ -133,9 +131,9 @@ The entry point is `define` (or `newHandler` for pre-built options) — not a "r | `GracefulShutdownTimeout` | 5 seconds | | `ShutdownDeadlineBuffer` | 7 seconds | -`MaxWorkflowThreadCount` has no counterpart in the other SDKs — Java runs Workflow code on real threads. +`MaxWorkflowThreadCount` exists because Java runs Workflow code on real threads. -Eager Activities are disabled: `builder.setDisableEagerExecution(true)` (`LambdaWorkerOptions.java:258`). `ShutdownDeadlineBuffer` defaults to `GracefulShutdownTimeout` + 2s, the same relationship as the other SDKs. +Eager Activities are disabled: `builder.setDisableEagerExecution(true)` (`LambdaWorkerOptions.java:258`). `ShutdownDeadlineBuffer` defaults to `GracefulShutdownTimeout` + 2s. ## Logging — the binding must be SLF4J 1.7.x @@ -147,7 +145,7 @@ The Java SDK compiles against `org.slf4j:slf4j-api:1.7.36`. A 2.x provider (`slf ``` -With a correct binding the module logs its own lifecycle unprompted, which is more than the other SDKs give you by default: +With a correct binding the module logs its own lifecycle unprompted: ``` [main] INFO io.temporal.aws.lambda.LambdaWorker - Temporal Lambda worker started @@ -240,9 +238,9 @@ aws lambda create-function \ ``` - `--runtime`: `java17` (or another supported Java version). -- `--handler`: `fully.qualified.Class::method` — **a different format from every other SDK**, which use `module.function` / `module.export`. Point it at the method that delegates to the `LambdaWorker.define` handler. +- `--handler`: **`fully.qualified.Class::method`.** Point it at the method that delegates to the `LambdaWorker.define` handler. - `--zip-file`: the shaded jar directly; no separate zip step. Switch to `--code S3Bucket=…,S3Key=…` once the jar exceeds 50 MB, which happens early in Java (see packaging above). -- **`HOME=/tmp` is not needed** — unlike the Go and TypeScript examples. Verified: the Java module never reads `HOME`, and a missing config file is non-fatal. → Connection configuration above. +- **`HOME=/tmp` is not needed.** → Connection configuration above. - `--memory-size`: the docs recommend starting at `1024` because "Java Workers typically need more memory than other runtimes," then adjusting from CloudWatch. A measured hello-world used **240 MB of 1024** (`Max Memory Used` in the invocation's REPORT line), so `512` is usually ample for small Workers — and since Lambda bills GB-seconds, halving memory halves the bill. Start at 1024, read the metric, then cut. @@ -313,6 +311,6 @@ java.lang.NullPointerException: Cannot invoke "SuspendableWorker.awaitTerminatio This is **not** a failure. It appears *after* Tasks have completed, is followed by `Temporal Lambda worker stopped`, a clean `END`/`REPORT`, and no timeout; Workflows complete correctly. Do not change configuration, IAM, or timeouts in response to it. Confirm it is benign by checking that the Workflow completed and that `REPORT` shows a duration below the deadline, then ignore it. -**Java — `ClassNotFoundException` / `NoClassDefFoundError` at first invocation.** The uber-jar was built without merging `META-INF/services`, or the handler string is wrong. Check the handler format first: Java uses `fully.qualified.Class::method`, not the `module.function` form every other SDK uses. Then verify the services merge — `unzip -p META-INF/services/io.grpc.ManagedChannelProvider` should list more than one provider. → Build and package above. +**Java — `ClassNotFoundException` / `NoClassDefFoundError` at first invocation.** The uber-jar was built without merging `META-INF/services`, or the handler string is wrong. Check the handler format first: it must be `fully.qualified.Class::method`. Then verify the services merge — `unzip -p META-INF/services/io.grpc.ManagedChannelProvider` should list more than one provider. → Build and package above. **Java — exec-format or `UnsupportedClassVersionError` at first invocation.** Bytecode targets a newer JDK than the runtime. Set `` (or the Gradle toolchain) to match `--runtime`. diff --git a/references/aws-lambda/sdk-python.md b/references/aws-lambda/sdk-python.md index cc07f24..5d46172 100644 --- a/references/aws-lambda/sdk-python.md +++ b/references/aws-lambda/sdk-python.md @@ -18,7 +18,7 @@ Read the installed API before generating code: python -c "import temporalio.contrib.aws.lambda_worker as m; help(m.LambdaWorkerConfig)" ``` -**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. +**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs, so check rather than pattern-match from another language. **Fastest path:** start from the language sample linked above — it has a working Worker, Workflow, and Activity already wired together. The handler example below imports the Workflow and Activity from separate modules (`my_workflows`, `my_activities`). When writing from scratch, create those modules with at least one registered Workflow (declaring a versioning behavior) and one Activity, and name the entry-point file to match the `--handler` you deploy (for example, `lambda_function.py` → `--handler lambda_function.lambda_handler`). diff --git a/references/aws-lambda/sdk-typescript.md b/references/aws-lambda/sdk-typescript.md index 9d98258..a89a51e 100644 --- a/references/aws-lambda/sdk-typescript.md +++ b/references/aws-lambda/sdk-typescript.md @@ -18,7 +18,7 @@ Check the installed version, then read its type declarations: npm ls @temporalio/lambda-worker ``` -**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. +**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs, so check rather than pattern-match from another language. ## Entry point From d3772dca99a9d2575f28831c4f76d4e3158d7847 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Thu, 3 Sep 2026 11:58:35 -0700 Subject: [PATCH 13/16] addressing review comment - reducing verbosity --- SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SKILL.md b/SKILL.md index 8384c69..f5081f3 100644 --- a/SKILL.md +++ b/SKILL.md @@ -116,7 +116,7 @@ Where the harness has a todo list, use it *in addition to* the printed checklist --auth-method api_key --retention-days 30 ``` - Pass the **base name only** — `tcld` appends `.` itself, so `-n my-app` yields `my-app.a2dd6`. The call is asynchronous and returns `requestStatus.state: Pending`; poll `tcld namespace get -n ` until `state` is `Active` (tens of seconds) before using it, and read the frontend address from its `uri.grpc` rather than assembling one. Add the Namespace to the inventory in step 8 — it outlives the Worker and is not removed by the AWS teardown. + Pass the **base name only** — `tcld` appends `.`, so `-n my-app` yields `my-app.a2dd6`. Creation is asynchronous: poll `tcld namespace get -n ` until `state` is `Active`, and take the frontend address from its `uri.grpc` rather than assembling one. Add the Namespace to the inventory in step 8 — it outlives the Worker and survives the AWS teardown. **`tcld namespace list` starts a device-code login when the CLI is not authenticated** — it does not fail — so it can open a Temporal Cloud session the user never asked for. Say it may do that before you run it, and surface the verification URL if it does. From 125c68a4a66d9f2210e90dac069ef784955bd555 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Thu, 3 Sep 2026 12:22:10 -0700 Subject: [PATCH 14/16] updated based on review comments --- SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SKILL.md b/SKILL.md index f5081f3..287a6e0 100644 --- a/SKILL.md +++ b/SKILL.md @@ -116,7 +116,7 @@ Where the harness has a todo list, use it *in addition to* the printed checklist --auth-method api_key --retention-days 30 ``` - Pass the **base name only** — `tcld` appends `.`, so `-n my-app` yields `my-app.a2dd6`. Creation is asynchronous: poll `tcld namespace get -n ` until `state` is `Active`, and take the frontend address from its `uri.grpc` rather than assembling one. Add the Namespace to the inventory in step 8 — it outlives the Worker and survives the AWS teardown. + Pass the **base name only** — `tcld` appends `.`, so `-n my-app` yields `my-app.` **`tcld namespace list` starts a device-code login when the CLI is not authenticated** — it does not fail — so it can open a Temporal Cloud session the user never asked for. Say it may do that before you run it, and surface the verification URL if it does. From 60ced1f7d428d6703db230d869f5e284c5f85d11 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Thu, 3 Sep 2026 13:39:05 -0700 Subject: [PATCH 15/16] addressed partial set of review comments --- references/aws-lambda/diagnostics.md | 4 +- references/aws-lambda/sdk-dotnet.md | 89 +++++++++++++--------------- references/aws-lambda/setup.md | 2 - 3 files changed, 43 insertions(+), 52 deletions(-) diff --git a/references/aws-lambda/diagnostics.md b/references/aws-lambda/diagnostics.md index 1f0b658..2d4ac73 100644 --- a/references/aws-lambda/diagnostics.md +++ b/references/aws-lambda/diagnostics.md @@ -125,9 +125,9 @@ Common errors include: ### Language-specific signatures -**No application logs at all, but the Worker clearly ran.** Three SDKs produce this same misleading silence by unrelated mechanisms, and in every case the Worker is healthy — only the logging is broken. Diagnose invocation health from Lambda's own runtime markers (`INIT_START`/`START`/`END`/`REPORT`) and CloudWatch metrics instead, then fix the binding. +**No application logs at all, but the Worker clearly ran.** SDK-specific logging configuration can suppress application logs even when the Worker is healthy. Diagnose invocation health from Lambda's runtime markers (`INIT_START`/`START`/`END`/`REPORT`) and CloudWatch metrics. -For SDK-specific diagnostics, see the selected SDK reference. +For the exact logging cause and fix, see the selected SDK reference. ### Check for Lambda timeout diff --git a/references/aws-lambda/sdk-dotnet.md b/references/aws-lambda/sdk-dotnet.md index 09e47eb..4e385dc 100644 --- a/references/aws-lambda/sdk-dotnet.md +++ b/references/aws-lambda/sdk-dotnet.md @@ -1,18 +1,18 @@ # .NET SDK on AWS Lambda - +Sources: [`Temporalio.Extensions.Aws.Lambda` 1.18.0](https://www.nuget.org/packages/Temporalio.Extensions.Aws.Lambda/1.18.0), [Lambda extension source](https://github.com/temporalio/sdk-dotnet/tree/90c4ef35e260ed5bf553de7ef458053da28e3912/src/Temporalio.Extensions.Aws.Lambda), [OpenTelemetry extension source](https://github.com/temporalio/sdk-dotnet/tree/90c4ef35e260ed5bf553de7ef458053da28e3912/src/Temporalio.Extensions.Aws.Lambda.OpenTelemetry), and the [maintained Lambda Worker sample](https://github.com/temporalio/samples-dotnet/tree/6aba4fb9ea08177e303352ec9a4c61e303cefb0e/src/LambdaWorker). Use this reference for .NET SDK-specific package, entry-point, Worker configuration, tuned defaults, observability, and diagnostic details. For shared AWS Lambda deployment, observability infrastructure, and diagnostic flow, see `setup.md`, `observability.md`, and `diagnostics.md`. ## Package -Import: `using Temporalio.Extensions.Aws.Lambda;` plus `Temporalio.Common` (for `WorkerDeploymentVersion`) and `Amazon.Lambda.Core` (for `ILambdaContext`). +Import: `using Temporalio.Extensions.Aws.Lambda;` plus `Temporalio.Common` (for `WorkerDeploymentVersion`) and `Amazon.Lambda.Core` (for `ILambdaContext`). -Install: `dotnet add package Temporalio.Extensions.Aws.Lambda` — a **separate NuGet package** from `Temporalio`, published in **lockstep** with it (both 1.18.0), the same relationship Java has. Published versions: 1.17.0 and 1.18.0. The package targets `netstandard2.0` and declares `Temporalio` 1.18.0 and `Amazon.Lambda.Core` 3.1.0. +Install: `dotnet add package Temporalio.Extensions.Aws.Lambda` — a **separate NuGet package** from `Temporalio`, published in **lockstep** with it (both 1.18.0). Published versions: 1.17.0 and 1.18.0. The package targets `netstandard2.0` and declares `Temporalio` 1.18.0 and `Amazon.Lambda.Core` 3.1.0. -OpenTelemetry lives in a **second package**, `Temporalio.Extensions.Aws.Lambda.OpenTelemetry` (also 1.18.0) — unlike Python, where OTel is an extra on the same package. → Observability below. +OpenTelemetry lives in a **second package**, `Temporalio.Extensions.Aws.Lambda.OpenTelemetry` (also 1.18.0). → Observability below. -- .NET: [.NET Lambda Worker sample](https://github.com/temporalio/samples-dotnet/tree/main/src/LambdaWorker) — `Worker/`, `Starter/`, and `Deploy/` (deploy, IAM-role, execution-role, and telemetry scripts plus a CloudFormation template), with a test project under `tests/LambdaWorker`. +- .NET: [.NET Lambda Worker sample](https://github.com/temporalio/samples-dotnet/tree/6aba4fb9ea08177e303352ec9a4c61e303cefb0e/src/LambdaWorker) — `Worker/`, `Starter/`, and `Deploy/` (deploy, IAM-role, execution-role, and telemetry scripts plus a CloudFormation template), with a test project under `tests/LambdaWorker`. List the real public API of the resolved package before generating code — the `.nupkg` is a zip and ships full XML documentation: @@ -24,17 +24,15 @@ unzip -p temporalio.extensions.aws.lambda..nupkg \ unzip -p ...nupkg Temporalio.Extensions.Aws.Lambda.nuspec | grep dependency ``` -**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs, so check rather than pattern-match from another language. +If sources disagree, use the installed artifact's public API, followed by the maintained sample and the prose documentation. ## Entry point -**`TemporalLambdaWorker.CreateHandler(version, configure)`** — returns a `Func` that your handler method delegates to. Overloads take either a synchronous `Action` or an asynchronous `Func` for setup that must await. A further overload takes `TemporalLambdaWorkerHandlerOptions`, which the XML docs describe as "internal test seams" — not for production use. - -`TemporalLambdaWorker.LoadClientConnectOptions(...)` is also public, for loading connection options with Lambda-aware config resolution outside the handler. +**`TemporalLambdaWorker.CreateHandler(version, configure)`** — returns a `Func` that your handler method delegates to. Overloads take either a synchronous `Action` or an asynchronous `Func` for setup that must await. A further overload taking `TemporalLambdaWorkerHandlerOptions` is an internal test seam and is not for production use. ## Configure callback -Receives a `TemporalLambdaWorkerOptions` with `ClientOptions`, `WorkerOptions`, `ShutdownDeadlineBuffer`, `ShutdownHooks`, and `AddShutdownHook(Func)`. The Task Queue and registrations go through `WorkerOptions` — an ordinary `TemporalWorkerOptions`, so `TaskQueue`, `AddWorkflow()` and `AddActivity(...)` behave exactly as they do for a long-lived Worker. The callback runs **per invocation** (Java is the outlier that runs its at cold start). +Receives a `TemporalLambdaWorkerOptions` with public `ClientOptions`, `WorkerOptions`, `ShutdownDeadlineBuffer`, and `AddShutdownHook(Func)` members. The Task Queue and registrations go through `WorkerOptions` — an ordinary `TemporalWorkerOptions`, so `TaskQueue`, `AddWorkflow()` and `AddActivity(...)` behave exactly as they do for a long-lived Worker. The callback runs **per invocation**. ## Versioning behavior @@ -51,11 +49,11 @@ public class MyWorkflow Or a Worker-level default through `DefaultVersioningBehavior` in `DeploymentOptions`. -**The .NET Worker-level default is `AutoUpgrade`**, whereas TypeScript's is `PINNED`. Defaults are not uniform across SDKs — never state one globally, and prefer setting the behavior explicitly per Workflow. +**The .NET Worker-level default is `AutoUpgrade`.** Prefer setting the behavior explicitly per Workflow. ## Handler example -A plain class exposes an async method that delegates to the handler returned by `TemporalLambdaWorker.CreateHandler`. +A plain class exposes an async method that delegates to the handler returned by `TemporalLambdaWorker.CreateHandler`. ```csharp namespace MyCompany.Temporal.Worker; @@ -106,7 +104,15 @@ Registrations go through `config.WorkerOptions`, an ordinary `TemporalWorkerOpti `TemporalWorkerOptions.LoggerFactory` is unset by default and "defaults to the client logger factory", which is also unset — so `Workflow.Logger` output is discarded. Activity `Console.WriteLine` still reaches CloudWatch, which makes the gap look selective rather than total. +Install the console logging provider: + +```bash +dotnet add package Microsoft.Extensions.Logging.Console +``` + ```csharp +using Microsoft.Extensions.Logging; + config.WorkerOptions.LoggerFactory = LoggerFactory.Create(b => b.AddSimpleConsole().SetMinimumLevel(LogLevel.Information)); ``` @@ -119,7 +125,7 @@ Loaded automatically from environment variables and an optional TOML config file 2. `temporal.toml` in the Lambda task root (typically `/var/task`). 3. `temporal.toml` in the current working directory. -The sample copies a `temporal.toml` into the publish directory before zipping, so it lands in the task root, and keeps the API key in `TEMPORAL_API_KEY` rather than in the file. Supplying an API key enables TLS automatically. +When using `temporal.toml`, copy it into the publish directory before zipping so it lands in the task root. Keep the API key in `TEMPORAL_API_KEY` rather than in the file; supplying an API key enables TLS automatically. **TLS caveat specific to .NET — set `SSL_CERT_FILE` or the first invocation fails.** AWS's .NET 8 Lambda images force-override `SSL_CERT_FILE`, which prevents the SDK's Rust core from loading system root CAs. Set it explicitly on the function: @@ -127,24 +133,20 @@ The sample copies a `temporal.toml` into the publish directory before zipping, s SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt # or /etc/ssl/certs/ca-certificates.crt ``` -**This is server-certificate verification, not client credentials.** The API key is unaffected and is not the problem — an API key auto-enables TLS, and TLS requires verifying Temporal Cloud's certificate chain against root CAs. The connection fails before authentication is ever attempted. - -No other SDK on Lambda needs this. → `diagnostics.md` for why. +**This is server-certificate verification, not client credentials.** The API key is unaffected and is not the problem — an API key auto-enables TLS, and TLS requires verifying Temporal Cloud's certificate chain against root CAs. The connection fails before authentication is ever attempted. See `diagnostics.md` for the corresponding failure signature and recovery steps. ## Build and package ### Native dependency — publish must be RID-specific -The .NET SDK wraps a **native Rust core** (`libtemporalio_sdk_core_c_bridge.so`). A portable publish does not include the Linux build of it, and the failure appears only at first invocation. Always publish for an explicit runtime identifier matching the function's architecture: +The .NET SDK wraps a **native Rust core** (`libtemporalio_sdk_core_c_bridge.so`). For Lambda, publish for an explicit runtime identifier matching the function's architecture and confirm the native library is present before creating the zip: | `--runtime` | `--architectures` | |---|---| | `linux-x64` | `x86_64` | | `linux-arm64` | `arm64` | -This is .NET's equivalent of Python's `manylinux` wheels and Go's `GOARCH`. - -Publish for an explicit Linux runtime identifier, then zip the publish output. +Publish for an explicit Linux runtime identifier, then zip the publish output. ```bash dotnet publish path/to/Worker.csproj \ @@ -158,19 +160,22 @@ dotnet publish path/to/Worker.csproj \ [[ -f ./publish/libtemporalio_sdk_core_c_bridge.so ]] || { echo "Publish output is missing the linux-x64 Temporal native bridge." >&2; exit 1; } -# If you use a temporal.toml / otel-collector-config.yaml, copy them in so they -# land in the Lambda task root: -cp temporal.toml otel-collector-config.yaml ./publish/ +# Copy each optional configuration file that this deployment uses so it lands +# in the Lambda task root: +if [[ -f temporal.toml ]]; then + cp temporal.toml ./publish/ +fi +if [[ -f otel-collector-config.yaml ]]; then + cp otel-collector-config.yaml ./publish/ +fi cd ./publish && zip -r ../function.zip . && cd .. ``` -Temporal's own deploy script asserts the file is present before zipping, which is worth copying. `--self-contained false` is correct: the `dotnet8` managed runtime supplies the framework. +Keep the native-library check before zipping. `--self-contained false` is correct because the `dotnet8` managed runtime supplies the framework. ## Deploy the Lambda function - - ```bash aws lambda create-function \ --function-name my-temporal-worker \ @@ -192,19 +197,18 @@ aws lambda create-function \ "SSL_CERT_FILE":"/etc/pki/tls/certs/ca-bundle.crt"}} ``` -Without it the **first invocation fails**, the Task Queue is never bound, and the Worker is never invoked again — an otherwise-correct deployment that simply does not work. AWS's .NET 8 Lambda images force-override `SSL_CERT_FILE`, which stops the SDK's Rust core from loading system root CAs. `/etc/ssl/certs/ca-certificates.crt` also works; try the other if one fails. This is server-certificate verification, unrelated to your API key. → `diagnostics.md`. +Without it the **first invocation fails**, the Task Queue is never bound, and the Worker is never invoked again. AWS's .NET 8 Lambda images force-override `SSL_CERT_FILE`, which stops the SDK's Rust core from loading system root CAs. `/etc/ssl/certs/ca-certificates.crt` also works; try the other if one fails. This is server-certificate verification, unrelated to your API key. → `diagnostics.md`. -- `--runtime`: `dotnet8` (the sample targets `net8.0`). +- `--runtime`: `dotnet8` for a `net8.0` build. - `--handler`: **`ASSEMBLY::NAMESPACE.TYPE::METHOD` — three colon-separated parts.** Getting this wrong presents as a handler-not-found error at first invocation. -- `--timeout 600` / `--memory-size 256`. → `setup.md` for how to choose these; the timeout only has to clear startup, and memory is what drives cost. +- `--timeout 600` / `--memory-size 256` are example values. The timeout must accommodate Worker startup and registration, Task and Activity processing, and graceful shutdown. Memory contributes directly to Lambda cost. → `setup.md` for how to choose both values. - `--architectures` must match the publish RID (`linux-x64` → `x86_64`, `linux-arm64` → `arm64`). -- Temporal's deploy script retries `create-function` up to 12 times to absorb IAM propagation delay on a freshly created execution role — the same behavior described under "A freshly created execution role may not be assumable immediately" in `setup.md`. ## Observability ### OTel package -A **second NuGet package**, separate from the Lambda extension itself: +A **second NuGet package**, separate from the Lambda extension itself: ```bash dotnet add package Temporalio.Extensions.Aws.Lambda.OpenTelemetry @@ -217,6 +221,8 @@ Published in lockstep with `Temporalio` and `Temporalio.Extensions.Aws.Lambda` ( The package contributes an extension method on the options object, applied inside the configure callback: ```csharp +using Temporalio.Extensions.Aws.Lambda.OpenTelemetry; + TemporalLambdaWorker.CreateHandler( new WorkerDeploymentVersion("my-app", "build-1"), config => @@ -226,25 +232,14 @@ TemporalLambdaWorker.CreateHandler( config.WorkerOptions.AddWorkflow().AddActivity(MyActivities.MyActivity); }); ``` - `ApplyOpenTelemetryDefaults()` configures metrics and tracing against the ADOT layer's collector. Telemetry must be exported before the invocation ends — keep any metrics export interval shorter than the Lambda timeout. ### ADOT layer setup -Attach an **ADOT Collector layer** for the target region and architecture. No language-specific auto-instrumentation layer is needed, because the OpenTelemetry SDK arrives as an ordinary package dependency — the same situation as Go and Java. The sample's prerequisites list the collector layer ARN as something you supply per region. - -`OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/otel-collector-config.yaml`. The sample copies `otel-collector-config.yaml` into the publish directory before zipping so it lands in the task root. - -### Telemetry IAM permissions +Attach an **ADOT Collector layer** for the target region and architecture. No language-specific auto-instrumentation layer is needed because the OpenTelemetry SDK arrives as an ordinary package dependency. Supply the collector layer ARN for the target region. -The sample ships an `enable-telemetry.sh` that adds an inline policy to the **execution** role and turns on active tracing — a concrete, copyable form of the permissions listed under "Required IAM permissions" in `observability.md`: - -- `logs:CreateLogGroup`, `logs:CreateLogStream`, `logs:PutLogEvents`, scoped to `arn:aws:logs:::log-group:/aws/lambda/:*` -- `xray:PutTraceSegments`, `xray:PutTelemetryRecords` on `*` -- `cloudwatch:PutMetricData` on `*` - -It then runs `aws lambda update-function-configuration --tracing-config Mode=Active`, without which traces do not appear under the `AWS::Lambda::Function` filter in X-Ray. +Set `OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/otel-collector-config.yaml` and copy `otel-collector-config.yaml` into the publish directory before zipping so it lands in the task root. For the shared Collector configuration, X-Ray enablement, and execution-role permissions, see `observability.md`. @@ -254,7 +249,7 @@ For the shared Collector configuration, X-Ray enablement, and execution-role per |---|---|---| | .NET | `TemporalWorkerOptions.LoggerFactory` is unset and "defaults to the client logger factory", which is also unset — so `Workflow.Logger` output is discarded. Activity `Console.WriteLine` still reaches CloudWatch, which makes the gap look selective rather than total | set `config.WorkerOptions.LoggerFactory` (e.g. `LoggerFactory.Create(b => b.AddSimpleConsole().SetMinimumLevel(LogLevel.Information))`) | -**.NET — `DllNotFoundException` / missing `libtemporalio_sdk_core_c_bridge.so` at first invocation.** The .NET SDK wraps a native Rust core, and a portable (non-RID) publish omits its Linux build. Republish with an explicit runtime identifier matching the function's architecture (`--runtime linux-x64` for `x86_64`, `linux-arm64` for `arm64`) and check the file is in the publish output before zipping. → Build and package above. +**.NET — `DllNotFoundException` / missing `libtemporalio_sdk_core_c_bridge.so` at first invocation.** Republish with an explicit runtime identifier matching the function's architecture (`--runtime linux-x64` for `x86_64`, `linux-arm64` for `arm64`) and check the native library is in the publish output before zipping. → Build and package above. **.NET — `NativeCertsNotFound` at first invocation, despite a correct address, Namespace, and API key.** @@ -267,8 +262,6 @@ System.InvalidOperationException: Connection failed: Server connection error: *Cause:* AWS's .NET 8 Lambda images force-override `SSL_CERT_FILE`, so the SDK's Rust core cannot load system root CAs. *Fix:* set `SSL_CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt` (or `/etc/ssl/certs/ca-certificates.crt`) on the function, then recover the binding as described under "Failed first invocation" in `diagnostics.md` — the failed validation invocation means no Task Queue was bound and Temporal will not retry on its own. -"Certs" here means the operating system's root CA store, not any credential of yours: an API key auto-enables TLS, and TLS requires verifying the *server's* certificate chain. The connection fails before authentication is attempted, so the API key, Namespace, invocation role, and External ID are all irrelevant. Two discriminators: the same credentials work from a local Worker against the same Namespace, and the stack trace ends in `ConnectAsync` rather than a Temporal API call. - -Only .NET is affected. Python uses the same Rust core (`temporalio/bridge/temporal_sdk_bridge.abi3.so`) but its runtime image does not override the variable; Java uses gRPC/Netty with the JVM truststore. +"Certs" here means the operating system's root CA store, not client credentials. An API key auto-enables TLS, and TLS requires verifying the server's certificate chain. The connection fails before authentication is attempted, so changing the API key, Namespace, invocation role, or External ID will not fix this error. **.NET — handler not found at first invocation.** The handler string has **three** colon-separated parts, `ASSEMBLY::NAMESPACE.TYPE::METHOD`. Compare against the assembly name (not the project name, if they differ) and the fully-qualified type. diff --git a/references/aws-lambda/setup.md b/references/aws-lambda/setup.md index 5760c7c..30083bd 100644 --- a/references/aws-lambda/setup.md +++ b/references/aws-lambda/setup.md @@ -17,7 +17,6 @@ This is the end-to-end golden path: connect, write the Worker, package and deplo - An AWS account with permissions to create and invoke Lambda functions and create IAM roles. For the exact operator actions and a preflight check, see `iam.md`. - The AWS-specific steps require the `aws` CLI installed and configured with your AWS credentials. You may also use the AWS Console or the AWS SDKs. - The Go SDK, Python SDK, TypeScript SDK, Java SDK, or .NET SDK, depending on your language. -- The build toolchain for that SDK, because Step 2 packages the Worker with it: `go`, `pip`, `npm`, Maven or Gradle, or the `dotnet` CLI. Having the SDK as a dependency is not enough — confirm the toolchain is installed before starting. - The `temporal` CLI, authenticated to the target Temporal Service — Steps 4–6 and the CLI troubleshooting paths use it. See "Temporal CLI and Cloud connection" below. The selected SDK reference links its maintained sample project. @@ -215,7 +214,6 @@ Lambda bills **GB-seconds** — allocated memory × billed duration, however idl | `TEMPORAL_TLS_CLIENT_CERT_PATH` | Path to the TLS client certificate file for mTLS authentication. | | `TEMPORAL_TLS_CLIENT_KEY_PATH` | Path to the TLS client key file for mTLS authentication. | | `TEMPORAL_API_KEY` | API key for API key authentication. Supplying it auto-enables TLS; mTLS cert paths are not needed. | -| `SSL_CERT_FILE` | **Required for .NET on Lambda.** Path to the system CA bundle — `/etc/pki/tls/certs/ca-bundle.crt` or `/etc/ssl/certs/ca-certificates.crt`. AWS's .NET 8 images override this variable in a way that prevents the SDK's Rust core from loading root CAs, so TLS fails on the first invocation. Not needed for Go, Python, TypeScript, or Java. | The serverless Worker packages read environment variables and configuration files automatically at startup. For the full list of supported environment variables, config file format, and profiles, see the Environment configuration docs (`/develop/environment-configuration`). From 40fed40f475bfb02860919a4923224ce72b608b5 Mon Sep 17 00:00:00 2001 From: Harish Narayanappa Date: Thu, 3 Sep 2026 14:40:03 -0700 Subject: [PATCH 16/16] addressing comments to leave java-specific changes out of this pr --- SKILL.md | 22 ++++------------------ references/aws-lambda/sdk-java.md | 28 +++++++++++++++------------- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/SKILL.md b/SKILL.md index 287a6e0..9c825a5 100644 --- a/SKILL.md +++ b/SKILL.md @@ -51,7 +51,7 @@ Follow these steps in order. Each step is provider-neutral; the concrete command > - Nothing gets created before you approve that list. After approval the middle stretch runs unattended. > - At the end you get a Workflow you can watch execute, a full inventory of everything created, and an offer to remove it all. -**Write the summary provider-neutral, because at that point you do not know the provider.** Say "your cloud account", never the name of a provider you have not been told. The same applies to the account, Namespace, and region: leave out what you have not been told rather than filling it in with a plausible guess. +**Write the summary provider-neutral, because at that point you do not know the provider.** It is one of the things step 1 asks. Say "your cloud account", never the name of a provider you have not been told. The same applies to the account, Namespace, and region: if a cheap read-only call has already told you (see step 1), name what you actually found; otherwise leave it out rather than filling it in with a plausible guess. Skip the summary for troubleshooting, inspection, and configuration-change tasks. Someone whose Worker is not being invoked does not need an overview of a deployment they have already done. @@ -91,9 +91,7 @@ Where the harness has a todo list, use it *in addition to* the printed checklist **A step is complete when its verification passed — not when its command exited zero.** Several commands in this workflow exit clean having done nothing: the traffic-shifting and key-revocation commands no-op when their confirmation prompt goes unanswered, and providers return from create and update calls while the resource is still settling. Check an item off against state you read back, not against an exit code. When a step's verification fails, say which step you are on and what it is blocked on rather than moving down the list. -1. **Scope the task.** **Ask** — never infer — the SDK language (Go, Python, TypeScript, Java, or .NET), the deployment target (Temporal Cloud or self-hosted — self-hosted has its own server prerequisites), the compute provider, and whether this is a new setup, a configuration change, or troubleshooting. Confirm the deployment target is compatible with the chosen provider — see "A Namespace on the target cloud provider is required" under Provider-neutral principles. Each changes the specifics. → `references/concepts.md` for what the user is building; `references//setup.md` for the compatibility and client-setup details. - - **Nothing but the Namespace lookup runs before these questions.** The order is: summary, then `tcld namespace list` to populate the Namespace options, then one batch of questions. Do not probe the environment first — which CLIs are installed, what the working directory contains, which account a credential resolves to, which SDKs are on the machine; steps 2 and 3 re-check what matters. A directory listing is never evidence of an SDK preference. +1. **Scope the task.** Identify the SDK language (Go, Python, TypeScript, Java, or .NET), the deployment target (Temporal Cloud or self-hosted — self-hosted has its own server prerequisites), the compute provider, and whether this is a new setup, a configuration change, or troubleshooting. Confirm the deployment target is compatible with the chosen provider — see "A Namespace on the target cloud provider is required" under Provider-neutral principles. Ensure a Temporal client/CLI is available and authenticated to the target. Each changes the specifics. → `references/concepts.md` for what the user is building; `references//setup.md` for the compatibility and client-setup details. **Put the compute provider in that batch of questions as a confirmable default, not a free choice.** Pre-select the supported provider from the table above and carry its support status in the option's description. The user confirms rather than chooses, so it costs no extra turn, but the provider is never something they were assumed into. Skip the question only when the request already names a provider. Do not restate any of this in a paragraph before the questions; the option description is where it belongs. @@ -105,21 +103,9 @@ Where the harness has a todo list, use it *in addition to* the printed checklist - **Summarize the ineligible ones in a single line** — "you also have 2 Namespaces on \, which this skill does not support" — rather than listing them individually or hiding them. A user who knows they have a Namespace and cannot find it in the list concludes the tool is broken; one line keeps them informed and explains the constraint. - **Name the account you are listing from and confirm it is the intended one** before showing anything. A stale credential lists a real account that is not the one the user means to deploy into, and every option under it looks authoritative. - **If more Namespaces are eligible than the question format can hold, print the labelled list and ask the user to name one.** Do not silently show only the first few. - - **Always include "create a new one" as an option**, even when eligible Namespaces exist. Offer it last, and say it is a live, billable Temporal Cloud resource. This also settles the compute-provider answer, since a Namespace can only be served by compute on its own cloud provider — so a mismatch is caught here rather than at connection time, several steps later. - **Creating a Namespace, when that is what they picked.** Confirm the *spelling* before creating — names cannot be changed afterwards — and take the region from `tcld account list-regions`, never from memory: it returns `CloudProviderRegion` with a `CloudProvider` field, and entries whose provider is empty are not usable here. Most accounts can provision in only one region per provider, so there is often nothing to ask. - - ```bash - tcld namespace create -n --region --cloud-provider aws \ - --auth-method api_key --retention-days 30 - ``` - - Pass the **base name only** — `tcld` appends `.`, so `-n my-app` yields `my-app.` - - **`tcld namespace list` starts a device-code login when the CLI is not authenticated** — it does not fail — so it can open a Temporal Cloud session the user never asked for. Say it may do that before you run it, and surface the verification URL if it does. - **Degrade gracefully if `tcld` is not authenticated.** Ask the user for the Namespace name rather than stopping to fix the login — they can copy it from the Cloud UI, where it appears on the Namespace page and in the URL. Ask for its region in the same batch of questions: the name alone does not tell you the provider, and a mismatch missed here surfaces at connection time instead. **Never source a Namespace, account, or resource identifier from shell history.** History is stale by construction — it is full of last quarter's accounts — and reading it to guess a deployment target produces confident, wrong answers. Take identifiers from the user or from an authenticated API call, and nowhere else. @@ -164,7 +150,7 @@ Where the harness has a todo list, use it *in addition to* the printed checklist 7. **Verify.** Start a Workflow on the Task Queue and confirm Temporal invokes the Worker — check the Workflow history in the Temporal UI and the compute provider's logs. If it does not progress, → `references//diagnostics.md`. -8. **Hand back the inventory first; offer teardown as the closing note.** Close with what now exists — compute unit and published build identifiers, roles, infrastructure stacks, region, deployment name and build ID, plus any Namespace or API key created during the run — and what the run actually did, including anything you worked around or deviated from. Say plainly that it is live and billable. +8. **Hand back the inventory first; offer teardown as the closing note.** The order is inventory → offer, never the reverse. Close with what now exists — compute unit and published build identifiers, roles, infrastructure stacks, region, deployment name and build ID — and what the run actually did, including anything you worked around or deviated from. Say plainly that it is live and billable. These names are only knowable from the run that created them, and reconstructing them later means scanning the user's account. **Do not write a teardown script before the user asks for one.** Generating it unprompted buries the inventory under a file they did not request, and the inventory is what they need in order to decide. End with a single line — *"Let me know if you want a teardown script to remove these resources"* — and stop there. Write the script, or run the teardown, when they take you up on it. → `references//setup.md` (Teardown). @@ -253,5 +239,5 @@ Most questions need 2–3 reference files. - **General SDK development patterns** (Workflows, Activities, signals, queries, Worker Versioning concepts): see `skill-temporal-developer`. - **Traditional Worker tuning** (slot suppliers, tuners, poller autoscaling, resource-based tuning): see `skill-temporal-workertuning`. -- **Temporal Cloud administration** (users, certificates, billing, and Namespace management generally): see `skill-temporal-ops`. The one exception is creating a Namespace to deploy into, which step 1 handles inline — a user standing up their first serverless Worker should not be sent to another skill mid-run. +- **Temporal Cloud administration** (Namespaces, users, certificates, billing): see `skill-temporal-ops`. - **CLI command reference** (beyond the serverless-specific flags): see `skill-temporal-cli`. diff --git a/references/aws-lambda/sdk-java.md b/references/aws-lambda/sdk-java.md index cf6401f..a1359c2 100644 --- a/references/aws-lambda/sdk-java.md +++ b/references/aws-lambda/sdk-java.md @@ -23,7 +23,7 @@ Install: `io.temporal:temporal-aws-lambda` — a **separate Maven artifact** fro `aws-lambda-java-core` (1.4.0) arrives transitively from `temporal-aws-lambda`; declare it explicitly if you compile against `RequestHandler`/`Context`. -- Java: [Java Lambda Worker sample](https://github.com/temporalio/samples-java/tree/main/lambda-worker) — three Gradle subprojects (`worker/` handler + greeting Workflow/Activity, `starter/` local client, `deploy/` IAM and deploy scripts plus a CloudFormation template) +- Java: [Java Lambda Worker sample](https://github.com/temporalio/samples-java/tree/main/lambda-worker) — three Gradle subprojects (`worker/` handler + greeting Workflow/Activity, `starter/` local client, `deploy/` IAM and deploy scripts plus a CloudFormation template) List the real public API of the resolved artifact before generating code: @@ -35,13 +35,15 @@ javap -cp 'io.temporal.aws.lambda.LambdaWorkerOptions$Builder' # curl -O https://repo1.maven.org/maven2/io/temporal/temporal-aws-lambda//temporal-aws-lambda--sources.jar ``` -**Ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs, so check rather than pattern-match from another language. +**A useful ordering when sources disagree:** the installed artifact first, the SDK's maintained samples second (they are built in CI, so they cannot reference a method that does not exist), the prose docs last. Entry-point names are not consistent across SDKs — Java's is `define`, not "run"-shaped like the others — so check rather than pattern-match from another language. ## Entry point -**`LambdaWorker.define(version, configure)`** — returns a `RequestHandler` that your handler class delegates to. There are four public overloads: `define` (2- and 3-arg) and `newHandler` (2- and 3-arg, taking a pre-built `LambdaWorkerOptions`). +**`LambdaWorker.define(version, configure)`** — returns a `RequestHandler` that your handler class delegates to. There are four public overloads: `define` (2- and 3-arg) and `newHandler` (2- and 3-arg, taking a pre-built `LambdaWorkerOptions`). -## Configure callback — two phases +Note that Java's entry point is not "run"-shaped like the other SDKs' (`RunWorker`, `run_worker`, `runWorker`) — confirm the method name against the version you install. + +## Configure callback — two phases, unlike the other SDKs Java splits configuration in a way no other SDK does, and the distinction matters: @@ -111,7 +113,7 @@ public final class LambdaFunction implements RequestHandler { } ``` -The entry point is `define` (or `newHandler` for pre-built options). Temporal's [sample handler](https://github.com/temporalio/samples-java/blob/main/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java) is the reference implementation. +The entry point is `define` (or `newHandler` for pre-built options) — not a "run"-shaped name like the other SDKs use. Temporal's [sample handler](https://github.com/temporalio/samples-java/blob/main/lambda-worker/worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java) is the reference implementation. ## Lambda-tuned defaults @@ -131,9 +133,9 @@ The entry point is `define` (or `newHandler` for pre-built options). Temporal's | `GracefulShutdownTimeout` | 5 seconds | | `ShutdownDeadlineBuffer` | 7 seconds | -`MaxWorkflowThreadCount` exists because Java runs Workflow code on real threads. +`MaxWorkflowThreadCount` has no counterpart in the other SDKs — Java runs Workflow code on real threads. -Eager Activities are disabled: `builder.setDisableEagerExecution(true)` (`LambdaWorkerOptions.java:258`). `ShutdownDeadlineBuffer` defaults to `GracefulShutdownTimeout` + 2s. +Eager Activities are disabled: `builder.setDisableEagerExecution(true)` (`LambdaWorkerOptions.java:258`). `ShutdownDeadlineBuffer` defaults to `GracefulShutdownTimeout` + 2s, the same relationship as the other SDKs. ## Logging — the binding must be SLF4J 1.7.x @@ -145,7 +147,7 @@ The Java SDK compiles against `org.slf4j:slf4j-api:1.7.36`. A 2.x provider (`slf ``` -With a correct binding the module logs its own lifecycle unprompted: +With a correct binding the module logs its own lifecycle unprompted, which is more than the other SDKs give you by default: ``` [main] INFO io.temporal.aws.lambda.LambdaWorker - Temporal Lambda worker started @@ -237,11 +239,11 @@ aws lambda create-function \ --environment file:///tmp/lambda-env.json ``` -- `--runtime`: `java17` (or another supported Java version). -- `--handler`: **`fully.qualified.Class::method`.** Point it at the method that delegates to the `LambdaWorker.define` handler. +- `--runtime`: `java17` (or another supported Java version). +- `--handler`: `fully.qualified.Class::method` — **a different format from every other SDK**, which use `module.function` / `module.export`. Point it at the method that delegates to the `LambdaWorker.define` handler. - `--zip-file`: the shaded jar directly; no separate zip step. Switch to `--code S3Bucket=…,S3Key=…` once the jar exceeds 50 MB, which happens early in Java (see packaging above). -- **`HOME=/tmp` is not needed.** → Connection configuration above. -- `--memory-size`: the docs recommend starting at `1024` because "Java Workers typically need more memory than other runtimes," then adjusting from CloudWatch. A measured hello-world used **240 MB of 1024** (`Max Memory Used` in the invocation's REPORT line), so `512` is usually ample for small Workers — and since Lambda bills GB-seconds, halving memory halves the bill. Start at 1024, read the metric, then cut. +- **`HOME=/tmp` is not needed** — unlike the Go and TypeScript examples. Verified: the Java module never reads `HOME`, and a missing config file is non-fatal. → Connection configuration above. +- `--memory-size`: the docs recommend starting at `1024` because "Java Workers typically need more memory than other runtimes," then adjusting from CloudWatch. A measured hello-world used **240 MB of 1024** (`Max Memory Used` in the invocation's REPORT line), so `512` is usually ample for small Workers — and since Lambda bills GB-seconds, halving memory halves the bill. Start at 1024, read the metric, then cut. @@ -311,6 +313,6 @@ java.lang.NullPointerException: Cannot invoke "SuspendableWorker.awaitTerminatio This is **not** a failure. It appears *after* Tasks have completed, is followed by `Temporal Lambda worker stopped`, a clean `END`/`REPORT`, and no timeout; Workflows complete correctly. Do not change configuration, IAM, or timeouts in response to it. Confirm it is benign by checking that the Workflow completed and that `REPORT` shows a duration below the deadline, then ignore it. -**Java — `ClassNotFoundException` / `NoClassDefFoundError` at first invocation.** The uber-jar was built without merging `META-INF/services`, or the handler string is wrong. Check the handler format first: it must be `fully.qualified.Class::method`. Then verify the services merge — `unzip -p META-INF/services/io.grpc.ManagedChannelProvider` should list more than one provider. → Build and package above. +**Java — `ClassNotFoundException` / `NoClassDefFoundError` at first invocation.** The uber-jar was built without merging `META-INF/services`, or the handler string is wrong. Check the handler format first: Java uses `fully.qualified.Class::method`, not the `module.function` form every other SDK uses. Then verify the services merge — `unzip -p META-INF/services/io.grpc.ManagedChannelProvider` should list more than one provider. → Build and package above. **Java — exec-format or `UnsupportedClassVersionError` at first invocation.** Bytecode targets a newer JDK than the runtime. Set `` (or the Gradle toolchain) to match `--runtime`.