diff --git a/plugins/temporal/skills/temporal-developer/SKILL.md b/plugins/temporal/skills/temporal-developer/SKILL.md index 471c806..50a1603 100644 --- a/plugins/temporal/skills/temporal-developer/SKILL.md +++ b/plugins/temporal/skills/temporal-developer/SKILL.md @@ -1,14 +1,14 @@ --- name: temporal-developer -description: Develop, debug, and manage Temporal applications across Python, TypeScript, Go, Java, .NET and Ruby. Use when the user is building workflows, activities, or workers with a Temporal SDK, debugging issues like non-determinism errors, stuck workflows, or activity retries, using Temporal CLI, Temporal Server, or Temporal Cloud, or working with durable execution concepts like signals, queries, heartbeats, versioning, continue-as-new, child workflows, or saga patterns. Also use when the user mentions "run a Temporal workflow from the CLI", "start a dev server", "run temporal server start-dev", "temporal workflow start", "temporal workflow execute", "temporal workflow signal", "temporal workflow query", "temporal workflow update". -version: 0.5.0 +description: Develop, debug, and manage Temporal applications across Python, TypeScript, Go, Java, .NET, Ruby, and Rust. Use when the user is building workflows, activities, or workers with a Temporal SDK, debugging issues like non-determinism errors, stuck workflows, or activity retries, using Temporal CLI, Temporal Server, or Temporal Cloud, or working with durable execution concepts like signals, queries, heartbeats, versioning, continue-as-new, child workflows, or saga patterns. Also use when the user mentions "run a Temporal workflow from the CLI", "start a dev server", "run temporal server start-dev", "temporal workflow start", "temporal workflow execute", "temporal workflow signal", "temporal workflow query", "temporal workflow update". +version: 0.6.0 --- # Skill: temporal-developer ## Overview -Temporal is a durable execution platform that makes workflows survive failures automatically. This skill provides guidance for building Temporal applications in Python, TypeScript, Go, Java, .NET, and Ruby. +Temporal is a durable execution platform that makes workflows survive failures automatically. This skill provides guidance for building Temporal applications in Python, TypeScript, Go, Java, .NET, Ruby, and Rust. ## Core Architecture @@ -59,6 +59,7 @@ Check if `temporal` CLI is installed. If not, follow the instructions at `refere - Java -> read `references/java/java.md` - .NET (C#) -> read `references/dotnet/dotnet.md` - Ruby -> read `references/ruby/ruby.md` + - Rust -> read `references/rust/rust.md` (in Public Preview) 2. Second, read appropriate `core` and language-specific references for the task at hand. ## Primary References @@ -71,6 +72,8 @@ Check if `temporal` CLI is installed. If not, follow the instructions at `refere - Language-specific info at `references/{your_language}/gotchas.md` - **`references/core/versioning.md`** - Versioning strategies and concepts - how to safely change workflow code while workflows are running - Language-specific info at `references/{your_language}/versioning.md` +- **`references/core/standalone-activities.md`** - Standalone Activities: run an Activity directly from a Client without a Workflow (Public Preview) + - Language-specific info at `references/{your_language}/standalone-activities.md` - **`references/core/troubleshooting.md`** - Decision trees, recovery procedures - **`references/core/error-reference.md`** - Common error types, workflow status reference - **`references/core/interactive-workflows.md`** - Testing signals, updates, queries diff --git a/plugins/temporal/skills/temporal-developer/references/core/determinism.md b/plugins/temporal/skills/temporal-developer/references/core/determinism.md index d24f868..751046f 100644 --- a/plugins/temporal/skills/temporal-developer/references/core/determinism.md +++ b/plugins/temporal/skills/temporal-developer/references/core/determinism.md @@ -90,6 +90,7 @@ Each Temporal SDK language provides a different level of protection against non- - Go: The Go SDK has no runtime sandbox. Therefore, non-determinism bugs will never be immediately appararent, and are usually only observable during replay. The optional `workflowcheck` static analysis tool can be used to check for many sources of non-determinism at compile time. - .NET: The .NET SDK has no sandbox. It uses a custom TaskScheduler and a runtime EventListener to detect invalid task scheduling. Developers must use `Workflow.*` safe alternatives (e.g., Workflow.DelayAsync instead of Task.Delay) and avoid non-deterministic .NET Task APIs. - Ruby: The Ruby SDK uses Illegal Call Tracing (via `TracePoint`) to detect forbidden method calls at runtime on the workflow fiber, combined with a Durable Fiber Scheduler that makes fiber operations deterministic. +- Rust: The Rust SDK has runtime nondeterminism detection for external async wake sources in Workflow code. Keep it enabled, use SDK primitives such as `ctx.timer()` and `temporalio_sdk::workflows::select!`, and still avoid synchronous nondeterminism by convention. Regardless of which SDK you are using, it is your responsibility to ensure that workflow code does not contain sources of non-determinism. Use SDK-specific tools as well as replay tests for doing so. diff --git a/plugins/temporal/skills/temporal-developer/references/core/standalone-activities.md b/plugins/temporal/skills/temporal-developer/references/core/standalone-activities.md new file mode 100644 index 0000000..7731bd9 --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/core/standalone-activities.md @@ -0,0 +1,160 @@ +> [!NOTE] +> Standalone Activities are in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +# Standalone Activities (Concepts) + +This document provides core conceptual explanations of Standalone Activities in Temporal. For language-specific implementation details, see `references/{your_language}/standalone-activities.md` for the language you are working in (Python, TypeScript, Java, .NET, Go). + +## What is a Standalone Activity? + +A **Standalone Activity** is a top-level Activity Execution started directly by a Client, without using a Workflow. It is Temporal's job queue — the simplest way to run a single durable, retryable task. + +The rule of thumb: + +- **Need to orchestrate multiple Activities?** Use a Workflow. +- **Just need to execute a single Activity?** Use a Standalone Activity. + +The same Activity Function code runs in both modes with no changes — the only difference is how it is invoked. An Activity defined for a Workflow can also be executed standalone, and the Worker that hosts it does not need to know how it will be invoked. + +Compared to wrapping a single Activity in a Workflow, a Standalone Activity: + +- Reduces billable actions in Temporal Cloud. +- Lowers latency for short-lived executions (fewer Worker round-trips). +- Lives in a separate ID space from Workflows. + +### Use cases + +Standalone Activities fit durable single-job processing where you don't need multi-step orchestration: + +- Sending an email +- Processing a webhook +- Syncing data +- Any single-function task that benefits from built-in retries and timeouts + +### Key features + +- Execute Activities as a top-level primitive, without Workflow overhead. +- Native async job lifecycle: **schedule → dispatch → process → result**. +- Arbitrary-length jobs, with heartbeats for progress tracking. +- **At-least-once execution by default**, with native retry policy and timeouts. +- **At-most-once execution** when the retry policy's maximum attempts is 1. +- Addressable by Activity ID / Run ID for result retrieval, cancellation, and termination. +- Deduplication via configurable conflict policies. +- Priority and fairness support. +- Full visibility — list and count executions. + +## Using Standalone Activities + +### Defining activities + +Defining standalone activities is IDENTICAL to defining activities callable from a workflow - there is no distinction AT ALL between the two at activity definition or worker configuration site. Follow language-specific guidance for how to normally define activities and configure workers to run them. + +### Calling and Interacting with Standalone Activities + +The CLI and every SDK exposes the same conceptual operations against a Standalone Activity (method names differ per language — see the language reference): + +- **Execute** — durably enqueue the Activity, wait for a Worker to run it, and return the result. +- **Start** — durably enqueue the Activity and return a handle immediately, without waiting. +- **Get handle** — rebind a handle to a previously started Activity by ID (and optionally Run ID). +- **Get result** — wait on a handle for completion. `execute` is equivalent to `start` followed by awaiting the handle's result. +- **Cancel / Terminate** — via the handle or CLI. + +**Choosing an Activity ID.** Every Standalone Activity call requires an **Activity ID**, which uniquely identifies that one call. It is the key you use later to get the result, describe, cancel, or terminate the Activity, and it is what conflict/reuse policies dedupe against. Use a **business-logic identifier** that uniquely identifies the call — for example `send-welcome-email:user-42`, `sync-invoice:INV-2026-001`, or `process-webhook:`. This makes Activities addressable and naturally deduplicated by your domain. Only if you genuinely have no meaningful business-level identifier should you generate a **UUID** to use as the Activity ID. + +Visibility operations are available as well: +- **List** — enumerate Standalone Activity Executions matching a query. Only Standalone Activities are returned; Activities running inside Workflows are not included. +- **Count** — return the total number of executions matching a query (running, completed, failed, etc. — not the number of queued tasks). +- **Describe** — via the handle or CLI. + +See below for a quick reference how to call these operations from the CLI rather than SDKs. + +> [!IMPORTANT] +> When using an SDK, these operations are owned by the Temporal Client, and belong **in your non-workflow application code**. It is INVALID to call an activity as a standalone activity from within a workflow: you instead should use standard within-workflow activity calls. + +**Currently Supported SDKs: Python, TypeScript, Java, .NET, Go** + +## Quick CLI Standalone Activity Man Page + +Ultimately, any standalone activity invocation code should live in your application code and use the appropriate SDK, but the Temporal CLI is a quick and easy way to test invoking standalone activities during development. All subcommands live under `temporal activity`. + +The key operations are: + +**Execute (start and wait for the result).** Blocks until the Activity completes and prints the result to stdout. Requires `--activity-id`, `--type`, `--task-queue`, and at least one of `--start-to-close-timeout` / `--schedule-to-close-timeout`: + +```bash +temporal activity execute \ + --activity-id my-activity-id \ + --type ComposeGreeting \ + --task-queue my-task-queue \ + --start-to-close-timeout 10s \ + --input '{"some-key": "some-value"}' +``` + +`--input` takes a JSON value; pass it multiple times for multiple positional arguments. `--input-file` is also a convenient option for larger inputs. The same required flags apply to `start` below. + +Reminder: `--activity-id` must be unique across all activity calls, as discussed above. + +**Start (do not wait).** Enqueues the Activity and prints the Activity ID and Run ID without blocking: + +```bash +temporal activity start \ + --activity-id my-activity-id \ + --type ComposeGreeting \ + --task-queue my-task-queue \ + --start-to-close-timeout 10s \ + --input '{"some-key": "some-value"}' +``` + +Outputs this JSON shape: + +```json +{ + "activityId": "my-activity-id", + "runId": "019e84d3-949a-7a0e-ae78-63b8a0b172bd", + "namespace": "default" +} +``` + +**Result (wait for a started Activity).** Waits for completion and prints the result. `--run-id` is optional and defaults to the latest run of that Activity ID: + +```bash +temporal activity result --activity-id my-activity-id +``` + +**Describe (current state of one Activity).** Shows status, run state, task queue, timeouts, attempt count, etc.: + +```bash +temporal activity describe --activity-id my-activity-id +``` + +**List / Count (visibility across many Activities).** Only Standalone Activity Executions are returned (Activities running inside Workflows are not): + +```bash +temporal activity list +temporal activity count +``` + +**Cancel / Terminate (stop an Activity).** `cancel` requests cooperative cancellation (surfaced to the Activity on its next heartbeat response); `terminate` forcefully ends it (Activity code cannot see or respond to it). Both accept `--reason`: + +```bash +temporal activity cancel --activity-id my-activity-id --reason "no longer needed" +temporal activity terminate --activity-id my-activity-id --reason "no longer needed" +``` + +## Observability + +All existing Activity metrics apply to Standalone Activities (scheduled, started, completed, failed, timed out, canceled). + +## Public Preview limitations + +- Pause, reset, and update options are not supported (scheduled for GA). +- The `TerminateExisting` conflict policy and `TerminateIfRunning` reuse policy are not yet supported. + +## Temporal CLI support + +- Requires **Temporal CLI v1.7.0+** and **Temporal Server v1.31.0+**. See `references/core/install_cli.md` if you need to update the CLI. +- The Temporal Dev Server (`temporal server start-dev`) has Standalone Activities enabled by default. + +## Temporal Cloud support + +Standalone Activities are available in Temporal Cloud as a Public Preview feature. Because the SDK client config loaders read environment variables and TOML profiles, the same code runs against a local server or Temporal Cloud with no code changes. diff --git a/plugins/temporal/skills/temporal-developer/references/core/versioning.md b/plugins/temporal/skills/temporal-developer/references/core/versioning.md index 3081dcb..d5b0863 100644 --- a/plugins/temporal/skills/temporal-developer/references/core/versioning.md +++ b/plugins/temporal/skills/temporal-developer/references/core/versioning.md @@ -8,7 +8,7 @@ Workflow versioning allows safe deployment of code changes without breaking runn 1. **Patching API** - Code-level version branching 2. **Workflow Type Versioning** - New workflow types for incompatible changes -3. **Worker Versioning** - Deployment-level control with Build IDs +3. **Worker Versioning** - Deployment-level routing with Worker Deployment Versions ## Why Versioning is Needed @@ -101,13 +101,16 @@ Create a new workflow type (e.g., `OrderWorkflowV2`) instead of patching. ### Concept -Manage versions at deployment level using Build IDs. Multiple worker versions can run simultaneously. +Manage versions through Worker Deployments. Multiple Worker Deployment Versions can run simultaneously, and each version is identified by a deployment name and Build ID. + +> [!IMPORTANT] +> This is the current Worker Deployment-based versioning model. Do not confuse it with the legacy Build ID-based Worker Versioning APIs, which manage compatibility sets directly. Those APIs are deprecated. ``` -Worker v1.0 (Build ID: abc123) +Worker Deployment Version (deployment: order-service, build: abc123) └── Handles workflows started on this version -Worker v2.0 (Build ID: def456) +Worker Deployment Version (deployment: order-service, build: def456) └── Handles new workflows └── Can also handle upgraded old workflows ``` @@ -116,7 +119,9 @@ Worker v2.0 (Build ID: def456) **Worker Deployment**: Logical service grouping (e.g., "order-service") -**Build ID**: Specific code version (e.g., git commit hash) +**Worker Deployment Version**: A specific snapshot identified by a Worker Deployment name and a Build ID + +**Build ID**: The code-version component of a Worker Deployment Version (e.g., a git commit hash) **Versioning Behaviors**: @@ -136,6 +141,49 @@ Worker v2.0 (Build ID: def456) - Workflows need bug fixes during execution - Still requires patching for version transitions +## Upgrading on Continue-as-New + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +Long-running Pinned Workflows that use Continue-as-New can upgrade to newer Worker Deployment Versions at the Continue-as-New boundary without patching. + +This pattern is for: + +- Entity Workflows that run for months or years +- Batch processing Workflows that checkpoint with Continue-as-New +- AI agent Workflows with long sleeps waiting for user input + +### How it works + +By default, Pinned Workflows stay on their original Worker Deployment Version even when they Continue-as-New. With the upgrade option enabled: + +1. Each Workflow run remains pinned to its version (no patching needed during a run). +2. The Temporal Server tells the Workflow when a new **Target Version** becomes available — that is, when the Workflow's Worker Deployment gets a new Current or Ramping Version that the Workflow would move to next. +3. When the Workflow performs Continue-as-New with the upgrade option, the new run starts on the Target Version. + +### Detection flag + +Active Workflows detect a Target Version change by checking a per-Workflow flag exposed on `WorkflowInfo` (called `target_worker_deployment_version_changed` in the docs). The flag is refreshed after each Workflow Task completes; check it from code that runs as part of a Workflow Task (for example, before accepting an Update, starting an Activity, or starting a child Workflow). See the per-language `references/{your_language}/versioning.md` for the SDK-specific call. + +### Triggering the new run + +When the flag is set, return a Continue-as-New error with the new run's initial Versioning Behavior set to `AutoUpgrade`. This makes the new run start on the Target Version of its Worker Deployment. The Workflow Type itself retains its Pinned annotation; only the *initial* behavior of the *new* run is overridden so it picks up the Target Version. Once the new run is on the new version, the per-Workflow-type annotation continues to apply on subsequent CaN. + +### Limitations + +- **Lazy moving only — sleeping Workflows do not auto-upgrade.** Send a Signal to wake an idle Workflow so it can check the flag. +- **Interface compatibility is your responsibility.** When continuing as new to a different version, the previous version's Workflow input must be compatible with the new version's Workflow definition. If incompatible, the new run may fail on its first Workflow Task. +- **Pinned Workflows only.** Auto-Upgrade Workflows already move to the Target Version at Workflow Task boundaries; this pattern adds nothing for them. + +### When to use this pattern + +- Workflow Type is Pinned **and** +- Workflow runs longer than your Worker Deployment Version lifetime **and** +- Workflow already uses Continue-as-New to bound Event History size. + +For long-running Workflows that cannot use Continue-as-New (e.g., compliance audits that need full history), use `AUTO_UPGRADE` with patching instead. + ## Choosing an Approach | Scenario | Recommended Approach | @@ -143,7 +191,8 @@ Worker v2.0 (Build ID: def456) | Small change, few running workflows | Patching API | | Major rewrite | Workflow Type Versioning | | Many short workflows, frequent deploys | Worker Versioning (PINNED) | -| Long-running workflows needing updates | Worker Versioning (AUTO_UPGRADE) + Patching | +| Long-running workflows, uses Continue-as-New | Worker Versioning (PINNED) + upgrade on Continue-as-New | +| Long-running workflows, no Continue-as-New | Worker Versioning (AUTO_UPGRADE) + Patching | | Quick fix, can wait for completion | Wait for workflows to complete | ## Best Practices diff --git a/plugins/temporal/skills/temporal-developer/references/dotnet/dotnet.md b/plugins/temporal/skills/temporal-developer/references/dotnet/dotnet.md index a7f1c54..b9e5a0b 100644 --- a/plugins/temporal/skills/temporal-developer/references/dotnet/dotnet.md +++ b/plugins/temporal/skills/temporal-developer/references/dotnet/dotnet.md @@ -55,9 +55,19 @@ public class GreetingWorkflow ```csharp using Temporalio.Client; +using Temporalio.Common.EnvConfig; using Temporalio.Worker; -var client = await TemporalClient.ConnectAsync(new("localhost:7233")); +var connectOptions = ClientEnvConfig.LoadClientConnectOptions(); +connectOptions.TargetHost ??= "localhost:7233"; +var client = await TemporalClient.ConnectAsync(connectOptions); + +using var tokenSource = new CancellationTokenSource(); +Console.CancelKeyPress += (_, eventArgs) => +{ + tokenSource.Cancel(); + eventArgs.Cancel = true; +}; using var worker = new TemporalWorker( client, @@ -65,7 +75,7 @@ using var worker = new TemporalWorker( .AddWorkflow() .AddAllActivities(new MyActivities())); -await worker.ExecuteAsync(); +await worker.ExecuteAsync(tokenSource.Token); ``` **Start the dev server:** Start `temporal server start-dev` in the background. @@ -76,8 +86,11 @@ await worker.ExecuteAsync(); ```csharp using Temporalio.Client; +using Temporalio.Common.EnvConfig; -var client = await TemporalClient.ConnectAsync(new("localhost:7233")); +var connectOptions = ClientEnvConfig.LoadClientConnectOptions(); +connectOptions.TargetHost ??= "localhost:7233"; +var client = await TemporalClient.ConnectAsync(connectOptions); var result = await client.ExecuteWorkflowAsync( (GreetingWorkflow wf) => wf.RunAsync("my name"), @@ -107,7 +120,7 @@ Console.WriteLine($"Result: {result}"); ### Worker Setup -- Connect client, create `TemporalWorker` with workflows and activities +- Load connection settings with `ClientEnvConfig.LoadClientConnectOptions()`, connect the client, and create `TemporalWorker` with workflows and activities - Use `AddWorkflow()` and `AddAllActivities(instance)` or `AddActivity(method)` ### Determinism @@ -199,4 +212,5 @@ See `references/dotnet/testing.md` for info on writing tests. - **`references/dotnet/advanced-features.md`** — Schedules, worker tuning, dependency injection - **`references/dotnet/data-handling.md`** — Data converters, payload encryption, etc. - **`references/dotnet/versioning.md`** — Patching API, workflow type versioning, Worker Versioning +- **`references/dotnet/standalone-activities.md`** — Standalone Activities: run an Activity directly from a Client without a Workflow (Public Preview). Concept overview at `references/core/standalone-activities.md`. - **`references/dotnet/determinism-protection.md`** — Runtime task detection, .NET Task determinism rules diff --git a/plugins/temporal/skills/temporal-developer/references/dotnet/standalone-activities.md b/plugins/temporal/skills/temporal-developer/references/dotnet/standalone-activities.md new file mode 100644 index 0000000..f31d5ce --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/dotnet/standalone-activities.md @@ -0,0 +1,156 @@ +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +## Overview + +Standalone Activities are Activities run independently of any Workflow, started directly from a Temporal Client — useful when you need a single durable, retryable task (job-queue style) and not multi-step orchestration. The same Activity method can be executed both as a Standalone Activity and as a Workflow Activity with no code changes. + +Standalone Activities are conceptually the same across all SDKs. Read the [cross-SDK concept file](references/core/standalone-activities.md) if you have not already, and then see below for the .NET SDK specific APIs for calling Standalone Activities. + +## Prerequisites + +- Temporal .NET SDK v1.12.0 or higher. +- Temporal CLI v1.7.0 or higher — see [Temporal CLI install instructions](references/core/install_cli.md) if needed. Dev server includes Standalone Activities support. +- For production, Temporal Server v1.31.0 or higher (or Temporal Cloud). + +## Hosting Activities on a Worker + +The Activity is defined just as activities normally are in Temporal. Worker registration is also the same. + +```csharp +using Microsoft.Extensions.Logging; +using Temporalio.Client; +using Temporalio.Common.EnvConfig; +using Temporalio.Worker; +using TemporalioSamples.StandaloneActivity; + +var connectOptions = ClientEnvConfig.LoadClientConnectOptions(); +connectOptions.TargetHost ??= "localhost:7233"; +connectOptions.LoggerFactory = LoggerFactory.Create(builder => + builder. + AddSimpleConsole(options => options.TimestampFormat = "[HH:mm:ss] "). + SetMinimumLevel(LogLevel.Information)); +var client = await TemporalClient.ConnectAsync(connectOptions); + +const string taskQueue = "standalone-activity-sample"; + +using var tokenSource = new CancellationTokenSource(); +Console.CancelKeyPress += (_, eventArgs) => +{ + tokenSource.Cancel(); + eventArgs.Cancel = true; +}; + +using var worker = new TemporalWorker( + client, + new TemporalWorkerOptions(taskQueue). + AddActivity(MyActivities.ComposeGreetingAsync)); // register whatever your activity(ies) is/are + +await worker.ExecuteAsync(tokenSource.Token); +``` + +## Calling and managing Standalone Activities + +Start and manage Standalone Activities from your application code using the Temporal Client. + +### Do not call from inside a Workflow + +Don't call `client.ExecuteActivityAsync` / `client.StartActivityAsync` or any other Standalone Activity APIs from inside a Workflow Definition — use Workflow-side activity invocation (`Workflow.ExecuteActivityAsync`) instead. + +### Connect a Client + +The Standalone Activity operations are methods on a connected `TemporalClient`. The examples below assume this `client`. + +```csharp +using Temporalio.Client; +using Temporalio.Common.EnvConfig; + +var connectOptions = ClientEnvConfig.LoadClientConnectOptions(); +connectOptions.TargetHost ??= "localhost:7233"; +var client = await TemporalClient.ConnectAsync(connectOptions); +``` + +### Execute (wait for result) + +Use `client.ExecuteActivityAsync(...)` to durably enqueue the Activity, wait for it to run on a Worker, and return the result. The activity options require `Id`, `TaskQueue`, and at least one of `ScheduleToCloseTimeout` or `StartToCloseTimeout`. + +#### With type checking + +Use when activity definitions are available in this language. Pass a lambda invoking the activity method: + +```csharp +// In practice, use a meaningful business identifier, like customer or transaction identifier +var activityId = Guid.NewGuid().ToString(); + +var result = await client.ExecuteActivityAsync( + () => MyActivities.ComposeGreetingAsync(new ComposeGreetingInput("Hello", "World")), + new(activityId, "standalone-activity-sample") + { + ScheduleToCloseTimeout = TimeSpan.FromSeconds(10), + }); +``` + +#### Without type checking + +Use when activity definitions are unavailable in this language (i.e. you can't import them). Pass the activity type name as a string and an argument array: + +```csharp +var result = await client.ExecuteActivityAsync( + "ComposeGreeting", + new object?[] { new ComposeGreetingInput("Hello", "World") }, + new(activityId, "standalone-activity-sample") + { + ScheduleToCloseTimeout = TimeSpan.FromSeconds(10), + }); +``` + +### Start (do not wait for result) + +Use `client.StartActivityAsync(...)` to durably enqueue the Activity and get back a handle without waiting for completion. This takes the **exact same arguments as `ExecuteActivityAsync`**. + +```csharp +var handle = await client.StartActivityAsync(...); +``` + +### Get a handle to an existing Activity execution + +Use `client.GetActivityHandle(...)` to attach a handle to a previously started Standalone Activity. Passing `null` as the run ID (the default) targets the latest run of that Activity ID. + +```csharp +// Without a known result type +var handle = client.GetActivityHandle("my-activity-id", runId: "the-run-id"); + +// With a known result type +var typedHandle = client.GetActivityHandle("my-activity-id", runId: "the-run-id"); +``` + +### Wait for the result of a handle + +```csharp +var result = await handle.GetResultAsync(); +``` + +Calling `ExecuteActivityAsync` is equivalent to `StartActivityAsync` followed by `await handle.GetResultAsync()`. + +### List Standalone Activities + +```csharp +await foreach (var info in client.ListActivitiesAsync( + "TaskQueue = 'standalone-activity-sample'")) // returns an IAsyncEnumerable +{ + Console.WriteLine( + $"ActivityID: {info.ActivityId}, Type: {info.ActivityType}, Status: {info.Status}"); +} +``` + +Only Standalone Activity Executions are returned; Activities running inside Workflows are not included. + +### Count Standalone Activities + +Use `client.CountActivitiesAsync(query)` to count matching executions; this takes the **exact same arguments as `ListActivitiesAsync`**. + +```csharp +var resp = await client.CountActivitiesAsync( + "TaskQueue = 'standalone-activity-sample'"); +Console.WriteLine($"Total activities: {resp.Count}"); +``` diff --git a/plugins/temporal/skills/temporal-developer/references/dotnet/versioning.md b/plugins/temporal/skills/temporal-developer/references/dotnet/versioning.md index 6371926..8e4cd84 100644 --- a/plugins/temporal/skills/temporal-developer/references/dotnet/versioning.md +++ b/plugins/temporal/skills/temporal-developer/references/dotnet/versioning.md @@ -296,6 +296,49 @@ temporal workflow list --query \ 'TemporalWorkerDeploymentVersion = "my-service:v1.0.0" AND ExecutionStatus = "Running"' ``` +## Upgrading on Continue-as-New + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +For long-running Pinned Workflows that use Continue-as-New, detect a new Target Worker Deployment Version on `Workflow.TargetWorkerDeploymentVersionChanged` and continue-as-new with `InitialVersioningBehavior.AutoUpgrade` so the new run starts on the Target Version. See `references/core/versioning.md` for the conceptual model. + +### Detecting the Target Version change + +`Workflow.TargetWorkerDeploymentVersionChanged` is `true` when a new Current or Ramping Version is available for this Workflow's Worker Deployment. The flag is refreshed after each Workflow Task completes. + +Check the flag from code that runs as part of a Workflow Task — for example, before accepting an Update, starting an Activity, or starting a child Workflow. + +### Continue-as-new with upgrade + +When the flag is set, throw the exception from `Workflow.CreateContinueAsNewException`, passing a `ContinueAsNewOptions` whose `InitialVersioningBehavior` is `AutoUpgrade`, so the new run starts on the Target Version of its Worker Deployment. + +```csharp +using Temporalio.Common; +using Temporalio.Workflows; + +// At a natural Workflow Task boundary, e.g. before accepting Updates, +// starting Activities, starting child Workflows, etc.: +if (Workflow.TargetWorkerDeploymentVersionChanged) +{ + throw Workflow.CreateContinueAsNewException( + (MyWorkflow wf) => wf.RunAsync(nextInput), + new ContinueAsNewOptions + { + InitialVersioningBehavior = InitialVersioningBehavior.AutoUpgrade, + }); +} +``` + +> [!IMPORTANT] +> Don't busy-poll the flag on a timer. Check it at a natural Workflow Task boundary — before accepting Updates, starting Activities, starting child Workflows, etc. For idle Workflows, send a Signal to wake them so they can check it (see Limitations). + +### Limitations + +- **Lazy moving only — idle Workflows do not upgrade.** Send a Signal to wake an idle Workflow so it can check `TargetWorkerDeploymentVersionChanged`. +- **Workflow input must remain compatible across versions.** The new version's Workflow definition must accept the previous version's input; otherwise the new run may fail on its first Workflow Task. +- **Pinned Workflow Types only.** Auto-Upgrade Workflows move at Workflow Task boundaries already; the upgrade-on-CaN pattern adds nothing for them. + ## Best Practices 1. **Check for open executions** before removing old code paths diff --git a/plugins/temporal/skills/temporal-developer/references/go/go.md b/plugins/temporal/skills/temporal-developer/references/go/go.md index 6c42bed..3e259de 100644 --- a/plugins/temporal/skills/temporal-developer/references/go/go.md +++ b/plugins/temporal/skills/temporal-developer/references/go/go.md @@ -9,7 +9,7 @@ The Temporal Go SDK (`go.temporal.io/sdk`) provides a strongly-typed, idiomatic **Add Dependency:** In your Go module, add the Temporal SDK: ```bash -go get go.temporal.io/sdk +go get go.temporal.io/sdk go.temporal.io/sdk/contrib/envconfig ``` **workflows/greeting.go** - Workflow definition: @@ -67,11 +67,12 @@ import ( "yourmodule/workflows" "go.temporal.io/sdk/client" + "go.temporal.io/sdk/contrib/envconfig" "go.temporal.io/sdk/worker" ) func main() { - c, err := client.Dial(client.Options{}) + c, err := client.Dial(envconfig.MustLoadDefaultClientOptions()) if err != nil { log.Fatalln("Unable to create client", err) } @@ -107,10 +108,11 @@ import ( "github.com/google/uuid" "go.temporal.io/sdk/client" + "go.temporal.io/sdk/contrib/envconfig" ) func main() { - c, err := client.Dial(client.Options{}) + c, err := client.Dial(envconfig.MustLoadDefaultClientOptions()) if err != nil { log.Fatalln("Unable to create client", err) } @@ -157,7 +159,7 @@ func main() { ### Worker Setup -- Create client with `client.Dial(client.Options{})` +- Load file- and environment-based connection settings with `envconfig.MustLoadDefaultClientOptions()`, then pass them to `client.Dial` - Create worker with `worker.New(c, "task-queue", worker.Options{})` - Register workflows and activities - Run with `w.Run(worker.InterruptCh())` @@ -252,3 +254,4 @@ See `references/go/testing.md` for info on writing tests. - **`references/go/data-handling.md`** - Data converters, payload codecs, encryption - **`references/go/versioning.md`** - Patching API (`workflow.GetVersion`), Worker Versioning - **`references/go/determinism-protection.md`** - Information on **`workflowcheck`** tool to help statically check for determinism issues. +- **`references/go/standalone-activities.md`** - Standalone Activities (Public Preview): run an Activity directly from a Client without a Workflow; see also `references/core/standalone-activities.md` for cross-SDK concepts. diff --git a/plugins/temporal/skills/temporal-developer/references/go/standalone-activities.md b/plugins/temporal/skills/temporal-developer/references/go/standalone-activities.md new file mode 100644 index 0000000..694ff4a --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/go/standalone-activities.md @@ -0,0 +1,195 @@ +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +## Overview + +Standalone Activities are Activities run independently of any Workflow, started directly from a Temporal Client — useful when you need a single durable, retryable task (job-queue style) and not multi-step orchestration. The same Activity method can be executed both as a Standalone Activity and as a Workflow Activity with no code changes. + +Standalone Activities are conceptually the same across all SDKs. Read the [cross-SDK concept file](references/core/standalone-activities.md) if you have not already, and then see below for the Go SDK specific APIs for calling Standalone Activities. + +## Prerequisites + +- Temporal Go SDK v1.41.0 or higher. +- Temporal CLI v1.7.0 or higher — see [Temporal CLI install instructions](references/core/install_cli.md) if needed. The Temporal Dev Server has Standalone Activities enabled by default. +- For production, Temporal Server v1.31.0 or higher (or Temporal Cloud). + +## Hosting Activities on a Worker + +The Activity is defined just as activities normally are in Temporal. Worker registration is also the same. + +```go +package main + +import ( + "github.com/temporalio/samples-go/standalone-activity/helloworld" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/contrib/envconfig" + "go.temporal.io/sdk/worker" + "log" +) + +func main() { + c, err := client.Dial(envconfig.MustLoadDefaultClientOptions()) + if err != nil { + log.Fatalln("Unable to create client", err) + } + defer c.Close() + + w := worker.New(c, "standalone-activity-helloworld", worker.Options{}) + + w.RegisterActivity(helloworld.Activity) + + err = w.Run(worker.InterruptCh()) + if err != nil { + log.Fatalln("Unable to start worker", err) + } +} +``` + +## Calling and managing Standalone Activities + +Start and manage Standalone Activities from your application code using the Temporal `Client`. + +### Do not call from inside a Workflow + +Don't call `client.ExecuteActivity` or any other Standalone Activity APIs from inside a Workflow Definition — use Workflow-side activity invocation (`workflow.ExecuteActivity(ctx, ...)`) instead. + +### Connect a Client + +The Standalone Activity operations are methods on a connected `Client`. The examples below assume this client `c`. + +```go +import ( + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/contrib/envconfig" + "context" +) + +c, err := client.Dial(envconfig.MustLoadDefaultClientOptions()) +if err != nil { + log.Fatalln("Unable to create client", err) +} +defer c.Close() +``` + +### Execute a Standalone Activity + +Use `client.ExecuteActivity(...)` to durably enqueue the Activity. It then returns an `ActivityHandle` immediately — it does not wait for completion. After that, call `handle.Get(ctx, &out)` to wait for the result. There is no separate `Start` function in the Go SDK; `ExecuteActivity` is the only entry point. + +`client.StartActivityOptions` requires `ID`, `TaskQueue`, and at least one of `ScheduleToCloseTimeout` or `StartToCloseTimeout`. + +#### With type checking + +Use when activity definitions are available in this language. Pass the activity function reference. + +Pass the Activity as a function reference: + +```go +activityOptions := client.StartActivityOptions{ + ID: "send-welcome-email:user-42", + TaskQueue: "standalone-activity-helloworld", + ScheduleToCloseTimeout: 10 * time.Second, +} + +handle, err := c.ExecuteActivity(context.Background(), activityOptions, helloworld.Activity, "Temporal") +if err != nil { + log.Fatalln("Unable to execute activity", err) +} + +log.Println("Started", "ActivityID", handle.GetID(), "RunID", handle.GetRunID()) + +var result string +err := handle.Get(context.Background(), &result) +if err != nil { + log.Fatalln("Activity failed", err) +} +log.Println("Activity result:", result) +``` + +#### Without type checking + +Use when activity definitions are unavailable in this language (i.e. you can't import them). Pass the activity type name as a string. + +```go +activityOptions := client.StartActivityOptions{ + ID: "send-welcome-email:user-42", + TaskQueue: "standalone-activity-helloworld", + ScheduleToCloseTimeout: 10 * time.Second, +} + +handle, err := c.ExecuteActivity(context.Background(), activityOptions, "Activity", "Temporal") +if err != nil { + log.Fatalln("Unable to execute activity", err) +} + +log.Println("Started", "ActivityID", handle.GetID(), "RunID", handle.GetRunID()) + +var result string +err := handle.Get(context.Background(), &result) +if err != nil { + log.Fatalln("Activity failed", err) +} +log.Println("Activity result:", result) +``` + +### Get a handle to an existing Activity execution + +Use `client.GetActivityHandle()` to attach a handle to a previously started Standalone Activity. Both `ActivityID` and `RunID` are required. + +```go +handle := c.GetActivityHandle(client.GetActivityHandleOptions{ + ActivityID: "send-welcome-email:user-42", + RunID: "the-run-id", +}) +``` + +### Wait for the result of a handle + +Call `handle.Get(ctx, &out)` to block until the Activity completes and deserialize its result into the provided pointer. If the Activity failed, the failure is returned as an error. + +```go +var result string +err := handle.Get(context.Background(), &result) +if err != nil { + log.Fatalln("Activity failed", err) +} +log.Println("Activity result:", result) +``` + +Calling `ExecuteActivity` and then `handle.Get(ctx, &out)` is the Go equivalent of the synchronous "Execute and wait" pattern that other SDKs offer as a single call. + +### List Standalone Activities + +```go +resp, err := c.ListActivities(context.Background(), client.ListActivitiesOptions{ + Query: "TaskQueue = 'standalone-activity-helloworld'", +}) +if err != nil { + log.Fatalln("Unable to list activities", err) +} + +for info, err := range resp.Results { // a range-over-func iterator that yields `(ActivityExecutionInfo, error)` pairs. + if err != nil { + log.Fatalln("Error iterating activities", err) + } + log.Printf("ActivityID: %s, Type: %s, Status: %v\n", + info.ActivityID, info.ActivityType, info.Status) +} +``` + +Only Standalone Activity Executions are returned; Activities running inside Workflows are not included. + +### Count Standalone Activities + +Use `client.CountActivities()` to count matching executions; this takes the **exact same arguments as `ListActivities`**. + +```go +resp, err := c.CountActivities(context.Background(), client.CountActivitiesOptions{ + Query: "TaskQueue = 'standalone-activity-helloworld'", +}) +if err != nil { + log.Fatalln("Unable to count activities", err) +} + +log.Println("Total activities:", resp.Count) +``` diff --git a/plugins/temporal/skills/temporal-developer/references/go/versioning.md b/plugins/temporal/skills/temporal-developer/references/go/versioning.md index c8f7280..06f2ff4 100644 --- a/plugins/temporal/skills/temporal-developer/references/go/versioning.md +++ b/plugins/temporal/skills/temporal-developer/references/go/versioning.md @@ -226,6 +226,47 @@ temporal workflow list --query \ 'TemporalWorkerDeploymentVersion = "my-service:v1.0.0" AND ExecutionStatus = "Running"' ``` +## Upgrading on Continue-as-New + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +For long-running Pinned Workflows that use Continue-as-New, detect a new Target Worker Deployment Version on `WorkflowInfo` and continue-as-new with `ContinueAsNewVersioningBehaviorAutoUpgrade` so the new run starts on the Target Version. See `references/core/versioning.md` for the conceptual model. + +### Detecting the Target Version change + +`workflow.GetInfo(ctx).GetTargetWorkerDeploymentVersionChanged()` returns `true` when a new Current or Ramping Version is available for this Workflow's Worker Deployment. The flag is refreshed after each Workflow Task completes. + +Check the flag from code that runs as part of a Workflow Task — for example, before accepting an Update, starting an Activity, or starting a child Workflow. + +### Continue-as-new with upgrade + +When the flag is set, return `workflow.NewContinueAsNewErrorWithOptions` with `InitialVersioningBehavior: workflow.ContinueAsNewVersioningBehaviorAutoUpgrade` so the new run starts on the Target Version of its Worker Deployment. + +```go +// At a natural Workflow Task boundary, e.g. before accepting Updates, +// starting Activities, starting child Workflows, etc.: +if workflow.GetInfo(ctx).GetTargetWorkerDeploymentVersionChanged() { + return "", workflow.NewContinueAsNewErrorWithOptions( + ctx, + workflow.ContinueAsNewErrorOptions{ + InitialVersioningBehavior: workflow.ContinueAsNewVersioningBehaviorAutoUpgrade, + }, + "ContinueAsNewWithVersionUpgrade", + nextInput, + ) +} +``` + +> [!IMPORTANT] +> Don't busy-poll the flag on a timer. Check it at a natural Workflow Task boundary — before accepting Updates, starting Activities, starting child Workflows, etc. For idle Workflows, send a Signal to wake them so they can check it (see Limitations). + +### Limitations + +- **Lazy moving only — idle Workflows do not upgrade.** Send a Signal to wake an idle Workflow so it can check `GetTargetWorkerDeploymentVersionChanged`. +- **Workflow input must remain compatible across versions.** The new version's Workflow definition must accept the previous version's input; otherwise the new run may fail on its first Workflow Task. +- **Pinned Workflow Types only.** Auto-Upgrade Workflows move at Workflow Task boundaries already; the upgrade-on-CaN pattern adds nothing for them. + ## Best Practices 1. **Keep GetVersion calls** even when only a single branch remains -- it guards against stale replays and simplifies future changes diff --git a/plugins/temporal/skills/temporal-developer/references/integrations.md b/plugins/temporal/skills/temporal-developer/references/integrations.md index 71b3169..af5953b 100644 --- a/plugins/temporal/skills/temporal-developer/references/integrations.md +++ b/plugins/temporal/skills/temporal-developer/references/integrations.md @@ -19,3 +19,10 @@ Temporal ships and supports a growing set of integrations with third-party frame | LangSmith tracing (`temporalio.contrib.langsmith`) | Python | Experimental Temporal Plugin that propagates LangSmith trace context across Worker boundaries; lets `@traceable` run inside Workflows and Activities | `references/python/integrations/langsmith.md` | `references/python/ai-patterns.md`, `references/core/ai-patterns.md` | | LangGraph (`temporalio.contrib.langgraph`, Pre-release) | Python | Runs LangGraph Graph-API and Functional-API code as Temporal Workflows - nodes/tasks can execute as either in-workflow or as Activities | `references/python/integrations/langgraph.md` | `references/python/ai-patterns.md`, `references/core/ai-patterns.md` | | Google ADK (`temporalio[google-adk]`) | Python | Durable Google ADK agents: model calls run through `TemporalModel`-wrapped Activities, tools via `activity_tool`, MCP toolsets via `TemporalMcpToolSet` | `references/python/integrations/google-adk.md` | `references/python/ai-patterns.md`, `references/core/ai-patterns.md` | +| Pydantic AI (`pydantic-ai[temporal]`) | Python | Durable agents through the `TemporalDurability` capability, with model requests, tool calls, and MCP communication executed as Temporal Activities | `references/python/integrations/pydantic-ai.md` | `references/python/ai-patterns.md`, `references/core/ai-patterns.md` | +| OpenTelemetry (`temporalio[opentelemetry]`) | Python | Distributed tracing for Temporal apps with OpenTelemetry | `references/python/integrations/opentelemetry.md` | `references/python/observability.md` | +| OpenTelemetry (`@temporalio/interceptors-opentelemetry`) | TypeScript | Distributed tracing for Temporal apps with OpenTelemetry | `references/typescript/integrations/opentelemetry.md` | `references/typescript/observability.md` | +| Braintrust (`braintrust[temporal]`, Public Preview) | Python | LLM observability + prompt management: `BraintrustPlugin` traces every Workflow/Activity, `wrap_openai` captures LLM calls, `start_span` adds custom context, `load_prompt` fetches Braintrust-managed prompts | `references/python/integrations/braintrust.md` | `references/python/ai-patterns.md`, `references/core/ai-patterns.md` | +| Braintrust (`@braintrust/temporal`) | TypeScript | LLM observability: `BraintrustTemporalPlugin` registers on Client + Worker to trace Workflow/Activity spans; canonical guide hosted by Braintrust | `references/typescript/integrations/braintrust.md` | `references/core/ai-patterns.md` | +| Mastra (`@mastra/temporal`, Public Preview) | TypeScript | Build-time transform of Mastra `createWorkflow`/`createStep` definitions into Temporal Workflows and Activities; `MastraPlugin` auto-registers Activities on the Worker | `references/typescript/integrations/mastra.md` | `references/typescript/typescript.md`, `references/core/ai-patterns.md` | +| Vercel AI SDK (`@temporalio/ai-sdk`, Public Preview) | TypeScript | Durable Vercel AI SDK agents: `AiSdkPlugin` wraps `generateText` and other AI SDK calls as Activities; `temporalProvider.languageModel()` provides the workflow-safe model; tools dispatch via `proxyActivities`; stateless MCP servers register through `mcpClientFactories` and are used in-workflow via `TemporalMCPClient` | `references/typescript/integrations/vercel-ai-sdk.md` | `references/core/ai-patterns.md` | diff --git a/plugins/temporal/skills/temporal-developer/references/java/java.md b/plugins/temporal/skills/temporal-developer/references/java/java.md index 05e4f47..7b7c2f3 100644 --- a/plugins/temporal/skills/temporal-developer/references/java/java.md +++ b/plugins/temporal/skills/temporal-developer/references/java/java.md @@ -12,6 +12,7 @@ Gradle: ```groovy implementation 'io.temporal:temporal-sdk:1.+' +implementation 'io.temporal:temporal-envconfig:1.+' ``` Maven: @@ -22,6 +23,11 @@ Maven: temporal-sdk [1.0,) + + io.temporal + temporal-envconfig + [1.0,) + ``` **GreetActivities.java** - Activity interface: @@ -102,18 +108,19 @@ public class GreetingWorkflowImpl implements GreetingWorkflow { package greetingapp; import io.temporal.client.WorkflowClient; +import io.temporal.envconfig.ClientConfigProfile; import io.temporal.serviceclient.WorkflowServiceStubs; import io.temporal.worker.Worker; import io.temporal.worker.WorkerFactory; public class GreetingWorker { - public static void main(String[] args) { - // Create gRPC stubs for local dev server (localhost:7233) - WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); - - // Create client - WorkflowClient client = WorkflowClient.newInstance(service); + public static void main(String[] args) throws Exception { + ClientConfigProfile profile = ClientConfigProfile.load(); + WorkflowServiceStubs service = + WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + WorkflowClient client = + WorkflowClient.newInstance(service, profile.toWorkflowClientOptions()); // Create factory and worker WorkerFactory factory = WorkerFactory.newInstance(client); @@ -140,15 +147,19 @@ package greetingapp; import io.temporal.client.WorkflowClient; import io.temporal.client.WorkflowOptions; +import io.temporal.envconfig.ClientConfigProfile; import io.temporal.serviceclient.WorkflowServiceStubs; import java.util.UUID; public class Starter { - public static void main(String[] args) { - WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs(); - WorkflowClient client = WorkflowClient.newInstance(service); + public static void main(String[] args) throws Exception { + ClientConfigProfile profile = ClientConfigProfile.load(); + WorkflowServiceStubs service = + WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + WorkflowClient client = + WorkflowClient.newInstance(service, profile.toWorkflowClientOptions()); GreetingWorkflow workflow = client.newWorkflowStub( GreetingWorkflow.class, @@ -187,6 +198,7 @@ public class Starter { ### Worker Setup +- Load connection settings with `ClientConfigProfile.load()` and use the profile to configure both service stubs and the client - `WorkflowServiceStubs` -- gRPC connection to Temporal Server - `WorkflowClient` -- client used by worker to communicate with server - `WorkerFactory` -- creates Worker instances @@ -263,6 +275,7 @@ See `references/java/testing.md` for info on writing tests. - **`references/java/advanced-features.md`** - Schedules, worker tuning, and more - **`references/java/data-handling.md`** - Data converters, Jackson, payload encryption - **`references/java/versioning.md`** - Patching API, workflow type versioning, Worker Versioning +- **`references/java/standalone-activities.md`** - Standalone Activities: run an Activity directly from a Client without a Workflow (Public Preview). Concept overview at `references/core/standalone-activities.md`. ### Java Integrations diff --git a/plugins/temporal/skills/temporal-developer/references/java/standalone-activities.md b/plugins/temporal/skills/temporal-developer/references/java/standalone-activities.md new file mode 100644 index 0000000..b1c7337 --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/java/standalone-activities.md @@ -0,0 +1,134 @@ +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +## Overview + +Standalone Activities are Activities run independently of any Workflow, started directly from a Temporal Client — useful when you need a single durable, retryable task (job-queue style) and not multi-step orchestration. The same Activity method can be executed both as a Standalone Activity and as a Workflow Activity with no code changes. + +Standalone Activities are conceptually the same across all SDKs. Read the [cross-SDK concept file](references/core/standalone-activities.md) if you have not already, and then see below for the Java SDK specific APIs for calling Standalone Activities. + +## Prerequisites + +- Temporal Java SDK v1.35.0 or higher. +- Temporal CLI v1.7.0 or higher — see [Temporal CLI install instructions](references/core/install_cli.md) if needed. Dev server includes Standalone Activities support. +- For production, Temporal Server v1.31.0 or higher (or Temporal Cloud). + +## Hosting Activities on a Worker + +The Activity is defined just as activities normally are in Temporal. Worker registration is also the same. + +```java +ClientConfigProfile profile = ClientConfigProfile.load(); +WorkflowServiceStubs service = + WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + +WorkflowClient client = WorkflowClient.newInstance(service, profile.toWorkflowClientOptions()); +WorkerFactory factory = WorkerFactory.newInstance(client); +Worker worker = factory.newWorker(TASK_QUEUE); +worker.registerActivitiesImplementations(new GreetingActivitiesImpl()); // register whatever your activity(ies) is/are +factory.start(); +``` + +## Calling and managing Standalone Activities + +Start and manage Standalone Activities from your application code using the Temporal Client. + +### Do not call from inside a Workflow + +Don't call `ActivityClient.execute` / `ActivityClient.start` or any other Standalone Activity APIs from inside a Workflow Definition — use Workflow-side activity invocation (`Workflow.newActivityStub(...)`) instead. + +### Connect a Client + +The Standalone Activity operations are methods on a connected `ActivityClient`. The examples below assume this `client`. + +```java +ActivityClient client = + ActivityClient.newInstance( + service, + ActivityClientOptions.newBuilder().setNamespace(profile.getNamespace()).build()); +``` + +### Execute (wait for result) + +Use `client.execute(...)` to durably enqueue the Activity, wait for it to run on a Worker, and return the result. `StartActivityOptions` must set `id`, `taskQueue`, and at least one of `startToCloseTimeout` or `scheduleToCloseTimeout`. + +#### With type checking + +Use when activity definitions are available in this language. The typed form takes the Activity interface class and an unbound method reference; the SDK infers the Activity type name and result type at runtime. + +```java +// In practice, use a meaningful business identifier, like customer or transaction identifier +String activityId = UUID.randomUUID().toString(); + +StartActivityOptions options = + StartActivityOptions.newBuilder() + .setId(activityId) + .setTaskQueue(TASK_QUEUE) + .setStartToCloseTimeout(Duration.ofSeconds(10)) + .build(); + +String result = + client.execute( + GreetingActivities.class, + GreetingActivities::composeGreeting, + options, + "Hello", + "World"); +``` + +#### Without type checking + +Use when activity definitions are unavailable in this language (i.e. you can't import them). Call the Activity by its string type name and pass the result class. + +```java +String result = client.execute("ComposeGreeting", String.class, options, "Hello", "World"); +``` + +### Start (do not wait for result) + +Use `client.start(...)` to durably enqueue the Activity and get back an `ActivityHandle` without waiting for completion. This takes the **exact same arguments as `execute`**. + +```java +ActivityHandle handle = client.start(...); +``` + +### Get a handle to an existing Activity execution + +Use `client.getHandle(...)` to attach a typed handle to a previously started Standalone Activity. Passing `null` as the run ID targets the latest run of that Activity ID. + +```java +ActivityHandle handle = client.getHandle("standalone-activity-id", null, String.class); +``` + +### Wait for the result of a handle + +```java +String result = handle.getResult(); +// or, for a non-blocking wait... +CompletableFuture future = handle.getResultAsync(); +``` + +Calling `execute` is equivalent to `start` followed by `getResult()`. + +### List Standalone Activities + +```java +client + .listExecutions("TaskQueue = '" + TASK_QUEUE + "'") // returns a Stream + .forEach( + info -> + System.out.printf( + "ActivityID: %s, Type: %s, Status: %s%n", + info.getActivityId(), info.getActivityType(), info.getStatus())); +``` + +Only Standalone Activity Executions are returned; Activities running inside Workflows are not included. + +### Count Standalone Activities + +Use `client.countExecutions(query)` to count matching executions; this takes the **exact same arguments as `listExecutions`**. + +```java +ActivityExecutionCount resp = client.countExecutions("TaskQueue = '" + TASK_QUEUE + "'"); +System.out.println("Total activities: " + resp.getCount()); +``` diff --git a/plugins/temporal/skills/temporal-developer/references/java/versioning.md b/plugins/temporal/skills/temporal-developer/references/java/versioning.md index 0e520f2..d138e98 100644 --- a/plugins/temporal/skills/temporal-developer/references/java/versioning.md +++ b/plugins/temporal/skills/temporal-developer/references/java/versioning.md @@ -271,6 +271,48 @@ temporal workflow count --query \ 'TemporalWorkerDeploymentVersion = "order-service:v1.0.0" AND ExecutionStatus = "Running"' ``` +## Upgrading on Continue-as-New + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +For long-running Pinned Workflows that use Continue-as-New, detect a new Target Worker Deployment Version on `Workflow.getInfo()` and continue-as-new with `InitialVersioningBehavior.AUTO_UPGRADE` so the new run starts on the Target Version. See `references/core/versioning.md` for the conceptual model. + +### Detecting the Target Version change + +`Workflow.getInfo().isTargetWorkerDeploymentVersionChanged()` returns `true` when a new Current or Ramping Version is available for this Workflow's Worker Deployment. The flag is refreshed after each Workflow Task completes. + +Check the flag from code that runs as part of a Workflow Task — for example, before accepting an Update, starting an Activity, or starting a child Workflow. + +### Continue-as-new with upgrade + +When the flag is set, call `Workflow.continueAsNew` with a `ContinueAsNewOptions` whose `InitialVersioningBehavior` is `AUTO_UPGRADE` so the new run starts on the Target Version of its Worker Deployment. + +```java +import io.temporal.common.InitialVersioningBehavior; +import io.temporal.workflow.ContinueAsNewOptions; +import io.temporal.workflow.Workflow; + +// At a natural Workflow Task boundary, e.g. before accepting Updates, +// starting Activities, starting child Workflows, etc.: +if (Workflow.getInfo().isTargetWorkerDeploymentVersionChanged()) { + Workflow.continueAsNew( + ContinueAsNewOptions.newBuilder() + .setInitialVersioningBehavior(InitialVersioningBehavior.AUTO_UPGRADE) + .build(), + nextInput); +} +``` + +> [!IMPORTANT] +> Don't busy-poll the flag on a timer. Check it at a natural Workflow Task boundary — before accepting Updates, starting Activities, starting child Workflows, etc. For idle Workflows, send a Signal to wake them so they can check it (see Limitations). + +### Limitations + +- **Lazy moving only — idle Workflows do not upgrade.** Send a Signal to wake an idle Workflow so it can check `isTargetWorkerDeploymentVersionChanged`. +- **Workflow input must remain compatible across versions.** The new version's Workflow definition must accept the previous version's input; otherwise the new run may fail on its first Workflow Task. +- **Pinned Workflow Types only.** Auto-Upgrade Workflows move at Workflow Task boundaries already; the upgrade-on-CaN pattern adds nothing for them. + ## Best Practices 1. **Check for open executions** before removing old code paths diff --git a/plugins/temporal/skills/temporal-developer/references/python/advanced-features.md b/plugins/temporal/skills/temporal-developer/references/python/advanced-features.md index 6ad8ae8..38db3f4 100644 --- a/plugins/temporal/skills/temporal-developer/references/python/advanced-features.md +++ b/plugins/temporal/skills/temporal-developer/references/python/advanced-features.md @@ -134,7 +134,7 @@ client = await Client.connect( - The only field is `resolution_interval_millis: int = 30000` — how often to re-resolve DNS, in milliseconds. - `DnsLoadBalancingConfig.default` is a pre-built instance with the default 30-second interval. -- `dns_load_balancing_config` defaults to 30 seconds if you don't pass anything explicitly. +- `dns_load_balancing_config` defaults to 30 seconds if you don't pass anything explicitly. - Pass `dns_load_balancing_config=None` to disable DNS load balancing entirely. ### Mutual exclusion with HTTP CONNECT proxy @@ -152,7 +152,7 @@ Normally, your `__init__` must have no arguments. However, if you add the `@work class MyWorkflow: @workflow.init def __init__(self, initial_value: str) -> None: - # This runs only on first execution, not replay + # This runs when the Workflow is instantiated, including during replay self._value = initial_value self._items: list[str] = [] diff --git a/plugins/temporal/skills/temporal-developer/references/python/integrations/braintrust.md b/plugins/temporal/skills/temporal-developer/references/python/integrations/braintrust.md new file mode 100644 index 0000000..bf26d5a --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/python/integrations/braintrust.md @@ -0,0 +1,197 @@ +# Temporal Braintrust Integration (Python) + +## Overview + +[Braintrust](https://braintrust.dev) is an LLM observability and prompt-management platform. The Temporal Python SDK integrates with it through `braintrust.contrib.temporal.BraintrustPlugin`, which traces every Workflow and Activity as a span in Braintrust and links client-initiated spans to the Workflows they start. + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +For Python AI patterns (Pydantic data converter, disabling client-side LLM retries, generic LLM Activity shape) read `references/python/ai-patterns.md`. For conceptual LLM patterns shared across SDKs read `references/core/ai-patterns.md`. + +## Prerequisites + +- An existing Temporal Python development environment as described in `references/python/python.md`. + +## Install + +```bash +uv add "braintrust[temporal]" +``` + +## Initialize the logger before the Client or Worker + +The Braintrust logger must be initialized **before** the Temporal Client and Worker are constructed so that spans connect correctly. + +```python +import os +from braintrust import init_logger + +init_logger(project=os.environ.get("BRAINTRUST_PROJECT", "my-project")) +``` + +`init_logger` takes a `project` argument that names the Braintrust project traces are written to. + +## Register `BraintrustPlugin` on the Client and the Worker + +Register `BraintrustPlugin` on **both** the Client and every Worker. The Worker registration produces Workflow/Activity spans; the Client registration propagates span context so client-side spans link to the Workflow they start. + +Client: + +```python +from temporalio.client import Client +from braintrust.contrib.temporal import BraintrustPlugin + +client = await Client.connect( + "localhost:7233", + plugins=[BraintrustPlugin()], +) +``` + +Worker: + +```python +from braintrust.contrib.temporal import BraintrustPlugin +from temporalio.worker import Worker + +worker = Worker( + client, + task_queue="my-task-queue", + workflows=[MyWorkflow], + activities=[my_activity], + plugins=[BraintrustPlugin()], +) +``` + +## API credentials + +The Worker process needs `BRAINTRUST_API_KEY` in its environment. The Client process that starts Workflow Executions does **not** need the Braintrust API key. + +```bash +export BRAINTRUST_API_KEY="your-api-key" +python worker.py +``` + +## Trace LLM calls with `wrap_openai` + +Wrap the OpenAI client with `braintrust.wrap_openai` so every chat/completion call is captured as a span with inputs, outputs, token counts, and latency. Pass `max_retries=0` so Temporal — not the OpenAI client — owns retries. + +```python +from braintrust import wrap_openai +from openai import AsyncOpenAI +from temporalio import activity + +@activity.defn +async def invoke_model(prompt: str) -> str: + client = wrap_openai(AsyncOpenAI(max_retries=0)) + + response = await client.chat.completions.create( + model="gpt-4o", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": prompt}, + ], + ) + + return response.choices[0].message.content +``` + +The resulting trace nests the OpenAI span under the Activity span, which sits under the Workflow span, which sits under the client-side span: + +``` +my-workflow-request (client span) +└── temporal.workflow.MyWorkflow + └── temporal.activity.invoke_model + └── Chat Completion (gpt-4o) +``` + +## Add custom spans with `start_span` + +Use `braintrust.start_span` from client code to capture application-level context (the user query, the final result) alongside the Workflow/Activity spans the plugin produces. + +```python +import uuid +from braintrust import start_span + +async def run_research(query: str): + with start_span(name="research-request", type="task") as span: + span.log(input={"query": query}) + + result = await client.execute_workflow( + ResearchWorkflow.run, + query, + id=f"research-{uuid.uuid4()}", + task_queue="research-task-queue", + ) + + span.log(output={"result": result}) + return result +``` + +## Manage prompts with `load_prompt` + +`braintrust.load_prompt(project=..., slug=...)` fetches a prompt managed in the Braintrust UI, so prompt edits go live without redeploying Workflow or Activity code. Call it from an Activity (model calls live in Activities), then call `prompt.build()` to get the prompt configuration; extract the message you need before invoking the LLM. + +```python +import os +import braintrust +from braintrust import wrap_openai +from openai import AsyncOpenAI +from temporalio import activity + +@activity.defn +async def invoke_model(prompt_slug: str, user_input: str) -> str: + prompt = braintrust.load_prompt( + project=os.environ.get("BRAINTRUST_PROJECT", "my-project"), + slug=prompt_slug, + ) + + built = prompt.build() + + system_content = "You are a helpful assistant." + for msg in built.get("messages", []): + if msg.get("role") == "system" and msg.get("content"): + system_content = msg["content"] + break + + client = wrap_openai(AsyncOpenAI(max_retries=0)) + + response = await client.chat.completions.create( + model="gpt-4o", + messages=[ + {"role": "system", "content": system_content}, + {"role": "user", "content": user_input}, + ], + ) + + return response.choices[0].message.content +``` + +### Fallback prompt for resilience + +Wrap `load_prompt` in a `try`/`except` and fall back to a hardcoded prompt so the Activity still runs if Braintrust is unreachable. + +```python +DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant." + +try: + prompt = braintrust.load_prompt(project="my-project", slug="my-prompt") + system_content = extract_system_message(prompt.build()) +except Exception as e: + activity.logger.warning(f"Failed to load prompt: {e}. Using fallback.") + system_content = DEFAULT_SYSTEM_PROMPT +``` + +## Common mistakes + +- **Initializing the Braintrust logger after constructing the Client or Worker.** Call `init_logger(...)` first; otherwise spans don't connect to the Worker process. +- **Registering `BraintrustPlugin` on only the Worker (or only the Client).** Register on both — the Client registration is what links client-side spans to Workflow executions. +- **Forgetting `max_retries=0` on the wrapped OpenAI client.** Temporal owns retries; leaving the OpenAI client's built-in retries on duplicates work and obscures retry counts in traces. +- **Calling `load_prompt` from inside a Workflow.** Prompt loading is an external I/O call; keep it in an Activity. +- **Setting `BRAINTRUST_API_KEY` only on the Client process.** The Worker is what calls Braintrust; the Client doesn't need the key. + +## Additional Resources + +- `references/python/ai-patterns.md` — Python LLM patterns (Pydantic, retry discipline, generic LLM Activity shape). +- `references/core/ai-patterns.md` — Conceptual LLM patterns shared across SDKs. +- [Deep research sample](https://github.com/braintrustdata/braintrust-cookbook/blob/main/examples/TemporalDeepResearch/TemporalDeepResearch.mdx) — end-to-end agent showing `BraintrustPlugin`, `wrap_openai`, `start_span`, and `load_prompt`. diff --git a/plugins/temporal/skills/temporal-developer/references/python/integrations/google-adk.md b/plugins/temporal/skills/temporal-developer/references/python/integrations/google-adk.md index 4d59f4d..3e0e015 100644 --- a/plugins/temporal/skills/temporal-developer/references/python/integrations/google-adk.md +++ b/plugins/temporal/skills/temporal-developer/references/python/integrations/google-adk.md @@ -9,7 +9,6 @@ The integration is built on the Python SDK [Plugin system](https://docs.temporal > [!NOTE] > This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. - For general Temporal AI/LLM patterns (retries, rate limits, multi-agent orchestration) see `references/core/ai-patterns.md` and `references/python/ai-patterns.md`. ## Prerequisites diff --git a/plugins/temporal/skills/temporal-developer/references/python/integrations/langgraph.md b/plugins/temporal/skills/temporal-developer/references/python/integrations/langgraph.md index 2675672..4237ca7 100644 --- a/plugins/temporal/skills/temporal-developer/references/python/integrations/langgraph.md +++ b/plugins/temporal/skills/temporal-developer/references/python/integrations/langgraph.md @@ -214,4 +214,4 @@ For LangSmith tracing of LangGraph nodes and Temporal Activities together, use t - `references/python/ai-patterns.md` — Python AI/LLM patterns (Pydantic data converter, LLM Activity design, retry/error classification). - `references/core/ai-patterns.md` — language-agnostic AI/LLM patterns. -- `references/python/integrations/langsmith.md` - Companion LangSmith plugin. +- `references/python/integrations/langsmith.md` - Companion LangSmith plugin. diff --git a/plugins/temporal/skills/temporal-developer/references/python/integrations/langsmith.md b/plugins/temporal/skills/temporal-developer/references/python/integrations/langsmith.md index 98db967..a0ab26a 100644 --- a/plugins/temporal/skills/temporal-developer/references/python/integrations/langsmith.md +++ b/plugins/temporal/skills/temporal-developer/references/python/integrations/langsmith.md @@ -100,7 +100,6 @@ The plugin makes `@traceable` replay-safe in the Workflow sandbox. You do not ne - The plugin injects metadata using `workflow.now()` for timestamps and `workflow.random()` for UUIDs instead of `datetime.now()` and `uuid4()`. - LangSmith HTTP calls run on a background thread pool that does not interfere with deterministic Workflow execution. - ## Context propagation Trace context flows automatically across Client → Workflow → Activity → Child Workflow → Nexus via Temporal headers. Do not pass context manually. diff --git a/plugins/temporal/skills/temporal-developer/references/python/integrations/openai-agents-sdk.md b/plugins/temporal/skills/temporal-developer/references/python/integrations/openai-agents-sdk.md index 3807eb7..8ddf36a 100644 --- a/plugins/temporal/skills/temporal-developer/references/python/integrations/openai-agents-sdk.md +++ b/plugins/temporal/skills/temporal-developer/references/python/integrations/openai-agents-sdk.md @@ -164,7 +164,6 @@ Note that the initial run context comes from the `context=...` argument you pass In addition, since a `@function_tool` runs in the workflow, they can also call Temporal activities or other durable primitives themselves. - **Don't put I/O, system clock, or sources of randomness inside a `@function_tool` body.** Make it an `@activity.defn` and wrap with `activity_as_tool` instead. ### Picking between the two diff --git a/plugins/temporal/skills/temporal-developer/references/python/integrations/opentelemetry.md b/plugins/temporal/skills/temporal-developer/references/python/integrations/opentelemetry.md new file mode 100644 index 0000000..efdd8c4 --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/python/integrations/opentelemetry.md @@ -0,0 +1,63 @@ +# Temporal OpenTelemetry Integration (Python) + +## Overview + +`temporalio.contrib.opentelemetry` wires OpenTelemetry tracing into Temporal through the `OpenTelemetryPlugin`. It propagates W3C TraceContext + Baggage across Client, Workflow, Activity, and Nexus code and supports replay-safe custom Workflow spans. + +For observability beyond OpenTelemetry tracing (metrics, logging, Search Attributes) read `references/python/observability.md`. + +> [!NOTE] +> This feature is Pre-release. It is acceptable to use it on behalf of a user, but inform them that it is Pre-release. + +## Install the plugin + +Install the `temporalio[opentelemetry]` extra plus the OpenTelemetry exporter packages you use. + +## `OpenTelemetryPlugin` + +Create a replay-safe tracer provider, set it globally before creating the Client, and register the plugin on the Client. Workers created from that Client inherit the plugin automatically. Application spans propagate by default; pass `OpenTelemetryPlugin(add_temporal_spans=True)` to also emit Temporal lifecycle spans. + +```python +import opentelemetry.trace +from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor +from temporalio.client import Client +from temporalio.contrib.opentelemetry import OpenTelemetryPlugin, create_tracer_provider + +provider = create_tracer_provider() +provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) +opentelemetry.trace.set_tracer_provider(provider) + +client = await Client.connect( + "localhost:7233", + plugins=[OpenTelemetryPlugin()], +) +``` + +Inside a Workflow, use standard OpenTelemetry APIs to create custom replay-safe spans: + +```python +from datetime import timedelta +from opentelemetry.trace import get_tracer +from temporalio import workflow + +@workflow.defn +class MyWorkflow: + @workflow.run + async def run(self) -> None: + tracer = get_tracer(__name__) + with tracer.start_as_current_span("workflow-operation"): + await workflow.execute_activity( + my_activity, + start_to_close_timeout=timedelta(seconds=30), + ) +``` + +## Common mistakes + +- **Registering the same plugin on both Client and Worker.** Register on the Client only; Workers inherit it. +- **Creating a Workflow Worker before installing the replay-safe global provider.** Set the provider returned by `create_tracer_provider(...)` globally before constructing a Worker that uses `OpenTelemetryPlugin`. +- **Building a plain `opentelemetry.sdk.trace.TracerProvider` and passing it to `set_tracer_provider`.** `OpenTelemetryPlugin` requires a `ReplaySafeTracerProvider`; build it with `create_tracer_provider(...)`. + +## Resources + +- SDK metrics and observability reference: `references/python/observability.md` diff --git a/plugins/temporal/skills/temporal-developer/references/python/integrations/pydantic-ai.md b/plugins/temporal/skills/temporal-developer/references/python/integrations/pydantic-ai.md new file mode 100644 index 0000000..46c20fe --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/python/integrations/pydantic-ai.md @@ -0,0 +1,250 @@ +# Temporal Pydantic AI Integration (Python) + +## Overview + +[Pydantic AI](https://ai.pydantic.dev/) ships first-party Temporal support in `pydantic_ai.durable_exec.temporal`. Add the `TemporalDurability` capability to a regular Pydantic AI `Agent`; inside a Temporal Workflow, it moves model requests, I/O tool calls, and MCP communication into Temporal Activities while the agent loop remains deterministic Workflow code. + +The same agent remains usable outside a Workflow as a normal, non-durable agent. Attaching the capability does not make calls durable by itself: the call to `agent.run()` must execute inside a Temporal Workflow started through a Temporal Client. + +This integration comes from Pydantic AI, not `temporalio.contrib`. For general design guidance, also read `references/core/ai-patterns.md` and `references/python/ai-patterns.md`. + +## Install + +Install the full package or the slim package with Temporal support: + +```bash +pip install "pydantic-ai[temporal]" +# or +pip install "pydantic-ai-slim[temporal]" +``` + +## Attach `TemporalDurability` + +Construct the agent at module scope and attach the capability through `capabilities=`: + +```python +from pydantic_ai import Agent +from pydantic_ai.durable_exec.temporal import TemporalDurability + +agent = Agent( + "openai:gpt-5.6-sol", + instructions="You answer geography questions.", + name="geography", + capabilities=[TemporalDurability()], +) +``` + +Module-scope construction lets the Worker discover and register every generated Activity before Workflow execution begins. Inside a Workflow, use the asynchronous agent API; `Agent.run_sync()` cannot drive Temporal's Workflow event loop, so call `await agent.run(...)` instead. + +### `TemporalDurability` configuration + +| Parameter | Purpose | +|---|---| +| `models` | Additional model instances keyed by stable IDs for runtime model switching. | +| `event_stream_handler` | Handles live model events inside model-request Activities and tool events in event-handler Activities. | +| `event_stream_topic` | Publishes events to a Temporal Workflow Stream topic for an external consumer. | +| `event_stream_events` | Filters which events are published to `event_stream_topic`. | +| `event_stream_batch_interval` | Controls Workflow Stream batching; defaults to 100 ms. | +| `name` | Overrides the agent name used in generated Activity names. | +| `deps_type` | Overrides the dependency type used for Temporal serialization. | +| `activity_config` | Base `ActivityConfig`; defaults to a 60-second `start_to_close_timeout`. | +| `model_activity_config` | Overrides the base config for model-request Activities. | +| `event_stream_handler_activity_config` | Overrides the base config for event-handler Activities. | +| `toolset_activity_config` | Per-toolset overrides keyed by stable toolset ID. | +| `run_context_type` | Custom `TemporalRunContext` subclass for the Activity boundary. | + +## Stable agent and toolset identity + +Generated Activity names depend on the agent `name` and toolset IDs. Set them explicitly, keep them unique, and do not rename them while Workflows using the old names may still replay. + +Dynamic toolsets require an explicit stable ID. Set `id=` when constructing a `DynamicToolset`, on `@agent.toolset`, or on a `DynamicCapability`. A capability that contributes tools should also have a stable capability ID. + +Factories for dynamic toolsets are re-resolved inside Activities and must produce the same result for the same dependencies. + +## Register the plugin on the Client + +Pass `PydanticAIPlugin()` to `Client.connect()`: + +```python +from temporalio.client import Client +from pydantic_ai.durable_exec.temporal import PydanticAIPlugin + +client = await Client.connect( + "localhost:7233", + plugins=[PydanticAIPlugin()], +) +``` + +The plugin supplies Pydantic-aware payload conversion, a compatible Workflow sandbox runner, Activity registration, and failure handling. Temporal propagates Client plugins that implement the Worker plugin protocol to Workers created from that Client. Do not pass the same `PydanticAIPlugin()` to `Worker`, because it would run twice. + +Do not also set `data_converter=pydantic_data_converter`; the plugin owns the payload-converter wiring. It preserves other `DataConverter` settings such as a payload codec, failure converter, or external storage. + +### Direct Activity registration + +Normally, list agents on `PydanticAIWorkflow.__pydantic_ai_agents__`. If changing the Worker is easier than changing the Workflow class, pass `AgentPlugin(agent)` to the Worker instead. Keep `PydanticAIPlugin()` on the Client for conversion and sandbox configuration. + +## Define and register the Workflow + +List every durable agent used by a Workflow in `__pydantic_ai_agents__`. These are regular `Agent` instances carrying `TemporalDurability`, not wrapper agents. + +```python +from temporalio import workflow +from pydantic_ai.durable_exec.temporal import PydanticAIWorkflow + + +@workflow.defn +class GeographyWorkflow(PydanticAIWorkflow): + __pydantic_ai_agents__ = [agent] + + @workflow.run + async def run(self, prompt: str) -> str: + result = await agent.run(prompt) + return result.output +``` + +`PydanticAIWorkflow` is optional but provides typing for `__pydantic_ai_agents__`. A Workflow using multiple agents should list each one. + +## Serialization and payload limits + +Values crossing between the Workflow and Activities must be Pydantic-serializable. This includes `deps`, model settings, run-context metadata, tool-call metadata, and tool metadata. Untyped dictionaries arrive in their JSON form, so tuples and sets become lists, models become dictionaries, and non-string dictionary keys become strings. Re-validate them when the receiving code needs a specific type. + +The Activity-side `RunContext` contains only the fields Pydantic AI serializes. Accessing unavailable fields such as `model`, `prompt`, `messages`, `model_settings`, or `tracer` raises `UserError`. Supply a custom `TemporalRunContext` through `run_context_type=` when an Activity requires additional serializable context. + +Treat dependency models and other persisted payload schemas as durable contracts. An incompatible type change can prevent a Worker from decoding existing Workflow history before user code runs. + +Temporal limits individual payloads to 2 MB by default, and binary data grows when base64-encoded. Keep large media and dependencies out of Workflow history by returning durable references or configuring Temporal external storage. Stored payloads must remain available for as long as their Workflow histories can replay. + +## Runtime models + +Model-name strings can cross the Activity boundary directly. The agent must have a model when it is constructed; that model is registered automatically as the default. + +Runtime `Model` instances cannot be reconstructed safely from only their model ID. Register each instance in `TemporalDurability(models={...})`, then select it by its stable key or pass that registered instance to `agent.run(model=...)`. + +For custom providers or credentials derived from `deps`, add a `ResolveModelId` capability before `TemporalDurability`. Its resolver runs again on the Worker and must be deterministic for a given model ID and dependencies; it must not perform external I/O. + +```python +from pydantic_ai import Agent +from pydantic_ai.capabilities import ResolveModelId +from pydantic_ai.durable_exec.temporal import TemporalDurability + +# Define `default_model`, `fast_model`, and `resolve_model` at module scope. +agent = Agent( + default_model, + name="multi-model", + capabilities=[ + ResolveModelId(resolve_model), + TemporalDurability(models={"fast": fast_model}), + ], +) +``` + +Executing toolsets that require durable wrapping must be attached when the agent is constructed so their Activities can be registered before the Worker starts. Runtime toolsets are limited to non-executing toolsets or function toolsets whose tools all opt out of Activity wrapping. + +## Activity configuration + +`activity_config` is the base for all generated Activities. `model_activity_config`, `event_stream_handler_activity_config`, and entries in `toolset_activity_config` merge over it. Pydantic AI validates these configs when constructing `TemporalDurability`, preventing an invalid key from repeatedly failing a Workflow Task at runtime. + +Per-tool configuration belongs in tool metadata: + +```python +from datetime import timedelta +from temporalio.workflow import ActivityConfig +from pydantic_ai.toolsets import FunctionToolset + +toolset = FunctionToolset(id="research") + +@toolset.tool( + metadata={ + "temporal": ActivityConfig( + start_to_close_timeout=timedelta(minutes=5), + ) + } +) +async def fetch_paper(arxiv_id: str) -> str: + ... +``` + +Use `metadata={"temporal": False}` to keep a non-I/O async tool in Workflow code. Synchronous tools cannot opt out because thread execution is non-deterministic. For third-party tools or groups of tools, apply the same metadata through `SetToolMetadata`. + +Generated Activities heartbeat in the background. Model Activities receive a 30-second heartbeat timeout by default; other Activity types receive one only when configured. Do not set a heartbeat timeout on code that can block the event loop long enough to prevent the heartbeat task from running. + +Temporal already retries failed Activities. Disable overlapping Pydantic AI HTTP retries and provider-client retries when possible, then configure the Temporal retry policy through `ActivityConfig`. + +## Logfire + +Register `LogfirePlugin` alongside `PydanticAIPlugin` on the Client: + +```python +from pydantic_ai.durable_exec.temporal import LogfirePlugin, PydanticAIPlugin + +client = await Client.connect( + "localhost:7233", + plugins=[PydanticAIPlugin(), LogfirePlugin()], +) +``` + +## End-to-end example + +```python +import asyncio +import uuid + +from temporalio import workflow +from temporalio.client import Client +from temporalio.worker import Worker + +from pydantic_ai import Agent +from pydantic_ai.durable_exec.temporal import ( + PydanticAIPlugin, + PydanticAIWorkflow, + TemporalDurability, +) + +agent = Agent( + "openai:gpt-5.6-sol", + instructions="You answer geography questions.", + name="geography", + capabilities=[TemporalDurability()], +) + + +@workflow.defn +class GeographyWorkflow(PydanticAIWorkflow): + __pydantic_ai_agents__ = [agent] + + @workflow.run + async def run(self, prompt: str) -> str: + result = await agent.run(prompt) + return result.output + + +async def main() -> None: + client = await Client.connect( + "localhost:7233", + plugins=[PydanticAIPlugin()], + ) + + async with Worker( + client, + task_queue="geography", + workflows=[GeographyWorkflow], + ): + result = await client.execute_workflow( + GeographyWorkflow.run, + args=["What is the capital of Mexico?"], + id=f"geography-{uuid.uuid4()}", + task_queue="geography", + ) + print(result) + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Resources + +- `references/python/ai-patterns.md` — Python AI/LLM patterns, payload conversion, and retry classification. +- `references/core/ai-patterns.md` — language-agnostic agent and tool-placement patterns. +- Upstream guide — [Pydantic AI durable execution with Temporal](https://pydantic.dev/docs/ai/capabilities/durable_execution/temporal/). +- Upstream API reference — [`pydantic_ai.durable_exec.temporal`](https://pydantic.dev/docs/ai/api/pydantic-ai/durable_exec/). diff --git a/plugins/temporal/skills/temporal-developer/references/python/observability.md b/plugins/temporal/skills/temporal-developer/references/python/observability.md index 0130d89..ab271b8 100644 --- a/plugins/temporal/skills/temporal-developer/references/python/observability.md +++ b/plugins/temporal/skills/temporal-developer/references/python/observability.md @@ -2,7 +2,9 @@ ## Overview -The Python SDK provides comprehensive observability through logging, metrics, tracing, and visibility (Search Attributes). +The Python SDK provides comprehensive observability through logging, metrics, tracing (OpenTelemetry), and visibility (Search Attributes). + +These pillars are complementary: **logging** (below) captures discrete events, **metrics** capture aggregate worker health, **tracing** stitches a single request across Client/Workflow/Activity/Nexus boundaries, and **Search Attributes** make executions queryable. ## Logging @@ -94,6 +96,10 @@ Runtime.set_default(runtime, error_if_already_set=True) - `temporal_activity_execution_latency` - Activity execution time - `temporal_workflow_task_replay_latency` - Replay duration +## Distributed Tracing (OpenTelemetry) + +See `references/python/integrations/opentelemetry.md`. + ## Search Attributes (Visibility) See the Search Attributes section of `references/python/data-handling.md` @@ -104,3 +110,4 @@ See the Search Attributes section of `references/python/data-handling.md` 2. Don't use print() in workflows - it will produce duplicate output on replay 3. Configure metrics for production monitoring 4. Use Search Attributes for business-level visibility +5. Use the `OpenTelemetryPlugin` for distributed tracing across Client/Workflow/Activity/Nexus boundaries. diff --git a/plugins/temporal/skills/temporal-developer/references/python/python.md b/plugins/temporal/skills/temporal-developer/references/python/python.md index 640a533..c48c6dc 100644 --- a/plugins/temporal/skills/temporal-developer/references/python/python.md +++ b/plugins/temporal/skills/temporal-developer/references/python/python.md @@ -42,6 +42,7 @@ class GreetingWorkflow: import asyncio import concurrent.futures from temporalio.client import Client +from temporalio.envconfig import ClientConfig from temporalio.worker import Worker # Import the activity and workflow from our other files @@ -49,9 +50,9 @@ from activities.greet import greet from workflows.greeting import GreetingWorkflow async def main(): - # Create client connected to server at the given address - # This is the default port for `temporal server start-dev` - client = await Client.connect("localhost:7233") + connect_config = ClientConfig.load_client_connect_config() + connect_config.setdefault("target_host", "localhost:7233") + client = await Client.connect(**connect_config) # Run the worker with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor: @@ -77,14 +78,16 @@ if __name__ == "__main__": ```python import asyncio from temporalio.client import Client +from temporalio.envconfig import ClientConfig import uuid # Import the workflow from the previous code from workflows.greeting import GreetingWorkflow async def main(): - # Create client connected to server at the given address - client = await Client.connect("localhost:7233") + connect_config = ClientConfig.load_client_connect_config() + connect_config.setdefault("target_host", "localhost:7233") + client = await Client.connect(**connect_config) # Execute a workflow result = await client.execute_workflow(GreetingWorkflow.run, "my name", id=str(uuid.uuid4()), task_queue="my-task-queue") @@ -119,7 +122,7 @@ See `sync-vs-async.md` for detailed guidance on choosing between sync and async. ### Worker Setup -- Connect client, create Worker with workflows and activities +- Load connection settings with `ClientConfig.load_client_connect_config()`, connect the client, and create a Worker with workflows and activities - Run the worker - Activities can specify custom executor @@ -180,6 +183,7 @@ See `references/python/testing.md` for info on writing tests. - **`references/python/advanced-features.md`** - Schedules, worker tuning, and more - **`references/python/data-handling.md`** - Data converters, Pydantic, payload encryption - **`references/python/versioning.md`** - Patching API, workflow type versioning, Worker Versioning +- **`references/python/standalone-activities.md`** - Standalone Activities: run an Activity directly from a Client without a Workflow (Public Preview). Concept overview at `references/core/standalone-activities.md`. - **`references/python/determinism-protection.md`** - Python sandbox specifics, forbidden operations, pass-through imports - **`references/python/ai-patterns.md`** - LLM integration, Pydantic data converter, AI workflow patterns - **`references/python/workflow-streams.md`** - Public-Preview `temporalio.contrib.workflow_streams` library: durable, offset-addressed event channel for streaming progress to subscribers. diff --git a/plugins/temporal/skills/temporal-developer/references/python/standalone-activities.md b/plugins/temporal/skills/temporal-developer/references/python/standalone-activities.md new file mode 100644 index 0000000..a7b2710 --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/python/standalone-activities.md @@ -0,0 +1,157 @@ +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +## Overview + +Standalone Activities are Activities run independently of any Workflow, started directly from a Temporal Client — useful when you need a single durable, retryable task (job-queue style) and not multi-step orchestration. The same Activity method can be executed both as a Standalone Activity and as a Workflow Activity with no code changes. + +Standalone Activities are conceptually the same across all SDKs. Read the [cross-SDK concept file](references/core/standalone-activities.md) if you have not already, and then see below for the Python SDK specific APIs for calling Standalone Activities. + +## Prerequisites + +- Temporal Python SDK v1.23.0 or higher. +- Temporal CLI v1.7.0 or higher — see [Temporal CLI install instructions](references/core/install_cli.md) if needed. Dev server includes Standalone Activities support. +- For production, Temporal Server v1.31.0 or higher (or Temporal Cloud). + +## Hosting Activities on a Worker + +The Activity is defined just as activities normally are in Temporal. Worker registration is also the same. + +```python +import asyncio +import concurrent.futures + +from temporalio.client import Client +from temporalio.envconfig import ClientConfig +from temporalio.worker import Worker + +from my_activity import compose_greeting + + +async def main(): + connect_config = ClientConfig.load_client_connect_config() + connect_config.setdefault("target_host", "localhost:7233") + client = await Client.connect(**connect_config) + with concurrent.futures.ThreadPoolExecutor(max_workers=100) as activity_executor: + worker = Worker( + client, + task_queue="my-standalone-activity-task-queue", + activities=[compose_greeting], # register whatever your activity(ies) is/are + activity_executor=activity_executor, + ) + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## Calling and managing Standalone Activities + +Start and manage Standalone Activities from your application code using the Temporal Client. + +### Do not call from inside a Workflow + +Don't call `client.execute_activity` / `client.start_activity` or any other Standalone Activity APIs from inside a Workflow Definition — use Workflow-side activity invocation (`workflow.execute_activity`) instead. + +### Connect a Client + +The Standalone Activity operations are methods on a connected `Client`. The examples below assume this `client`. + +```python +from temporalio.client import Client +from temporalio.envconfig import ClientConfig + +connect_config = ClientConfig.load_client_connect_config() +connect_config.setdefault("target_host", "localhost:7233") +client = await Client.connect(**connect_config) +``` + +### Execute (wait for result) + +Use `client.execute_activity(...)` to durably enqueue the Activity, wait for it to run on a Worker, and return the result. Required arguments: the activity (first positional), `args=[...]`, `id`, `task_queue`, and a timeout such as `start_to_close_timeout`. + +#### With type checking + +Use when activity definitions are available in this language. Pass the activity function reference; the SDK infers the result type from its signature. + +```python +import uuid +from datetime import timedelta + +# In practice, use a meaningful business identifier, like customer or transaction identifier +activity_id = str(uuid.uuid4()) + +activity_result = await client.execute_activity( + compose_greeting, + args=[ComposeGreetingInput("Hello", "World")], + id=activity_id, + task_queue="my-standalone-activity-task-queue", + start_to_close_timeout=timedelta(seconds=10), +) +``` + +#### Without type checking + +Use when activity definitions are unavailable in this language (i.e. you can't import them). Pass the activity type name as a string; optionally set `result_type` to decode the result. + +```python +from datetime import timedelta + +activity_result = await client.execute_activity( + "compose_greeting", + args=[ComposeGreetingInput("Hello", "World")], + id=activity_id, + task_queue="my-standalone-activity-task-queue", + start_to_close_timeout=timedelta(seconds=10), + result_type=str, +) +``` + +### Start (do not wait for result) + +Use `client.start_activity(...)` to durably enqueue the Activity and get back a handle without waiting for completion. This takes the **exact same arguments as `execute_activity`**. + +```python +activity_handle = await client.start_activity(...) +``` + +### Get a handle to an existing Activity execution + +Use `client.get_activity_handle(...)` to attach a handle to a previously started Standalone Activity. Omitting `run_id` (or passing `None`) targets the latest run of that Activity ID. + +```python +activity_handle = client.get_activity_handle(activity_id="my-standalone-activity-id") +``` + +### Wait for the result of a handle + +```python +result = await activity_handle.result() +``` + +Calling `execute_activity` is equivalent to `start_activity` followed by `await activity_handle.result()`. + +### List Standalone Activities + +```python +activities = client.list_activities( + query="TaskQueue = 'my-standalone-activity-task-queue'", +) # returns an async iterator of ActivityExecution + +async for info in activities: + print(f"ActivityID: {info.activity_id}, Type: {info.activity_type}, Status: {info.status}") +``` + +Only Standalone Activity Executions are returned; Activities running inside Workflows are not included. + +### Count Standalone Activities + +Use `client.count_activities(query=...)` to count matching executions; this takes the **exact same arguments as `list_activities`**. + +```python +resp = await client.count_activities( + query="TaskQueue = 'my-standalone-activity-task-queue'", +) +print("Total activities:", resp.count) +``` diff --git a/plugins/temporal/skills/temporal-developer/references/python/versioning.md b/plugins/temporal/skills/temporal-developer/references/python/versioning.md index c1ad39a..3f4dcdc 100644 --- a/plugins/temporal/skills/temporal-developer/references/python/versioning.md +++ b/plugins/temporal/skills/temporal-developer/references/python/versioning.md @@ -183,6 +183,9 @@ temporal workflow list --query 'WorkflowType = "PizzaWorkflow" AND ExecutionStat Worker Versioning manages versions at the deployment level, allowing multiple Worker versions to run simultaneously. +> [!IMPORTANT] +> Use the Worker Deployment APIs described below. The older Build ID-based APIs manage legacy compatibility sets and are deprecated. + ### Key Concepts **Worker Deployment**: A logical service grouping similar Workers together (e.g., "loan-processor"). All versions of your code live under this umbrella. @@ -192,11 +195,8 @@ Worker Versioning manages versions at the deployment level, allowing multiple Wo ### Configuring Workers for Versioning ```python -from temporalio.worker import Worker -from temporalio.worker.deployment_config import ( - WorkerDeploymentConfig, - WorkerDeploymentVersion, -) +from temporalio.common import WorkerDeploymentVersion +from temporalio.worker import Worker, WorkerDeploymentConfig worker = Worker( client, @@ -213,11 +213,16 @@ worker = Worker( ) ``` -**Configuration parameters:** +`WorkerDeploymentConfig` accepts exactly three parameters: +- `version`: A `WorkerDeploymentVersion` identifying this Worker Deployment Version - `use_worker_versioning`: Enables Worker Versioning -- `version`: Identifies the Worker Deployment Version (deployment name + build ID) -- Build ID: Typically a git commit hash, version number, or timestamp +- `default_versioning_behavior`: Fallback `VersioningBehavior` for Workflows that do not declare one + +`WorkerDeploymentVersion` accepts exactly two parameters: + +- `deployment_name`: The logical service name (e.g., "my-service") +- `build_id`: The code-version component, typically a git commit hash, version number, or timestamp ### PINNED vs AUTO_UPGRADE Behaviors @@ -322,6 +327,45 @@ temporal workflow list --query \ 'TemporalWorkerDeploymentVersion = "my-service:v1.0.0" AND ExecutionStatus = "Running"' ``` +## Upgrading on Continue-as-New + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +For long-running Pinned Workflows that use Continue-as-New, detect a new Target Worker Deployment Version on `workflow.info()` and continue-as-new with `ContinueAsNewVersioningBehavior.AUTO_UPGRADE` so the new run starts on the Target Version. See `references/core/versioning.md` for the conceptual model. + +### Detecting the Target Version change + +`workflow.info().is_target_worker_deployment_version_changed()` returns `True` when a new Current or Ramping Version is available for this Workflow's Worker Deployment. The flag is refreshed after each Workflow Task completes. + +Check the flag from code that runs as part of a Workflow Task — for example, before accepting an Update, starting an Activity, or starting a child Workflow. + +### Continue-as-new with upgrade + +When the flag is set, call `workflow.continue_as_new` with `initial_versioning_behavior=ContinueAsNewVersioningBehavior.AUTO_UPGRADE` so the new run starts on the Target Version of its Worker Deployment. + +```python +from temporalio import workflow +from temporalio.workflow import ContinueAsNewVersioningBehavior + +# At a natural Workflow Task boundary, e.g. before accepting Updates, +# starting Activities, starting child Workflows, etc.: +if workflow.info().is_target_worker_deployment_version_changed(): + workflow.continue_as_new( + next_input, + initial_versioning_behavior=ContinueAsNewVersioningBehavior.AUTO_UPGRADE, + ) +``` + +> [!IMPORTANT] +> Don't busy-poll the flag on a timer. Check it at a natural Workflow Task boundary — before accepting Updates, starting Activities, starting child Workflows, etc. For idle Workflows, send a Signal to wake them so they can check it (see Limitations). + +### Limitations + +- **Lazy moving only — idle Workflows do not upgrade.** Send a Signal to wake an idle Workflow so it can check `is_target_worker_deployment_version_changed`. +- **Workflow input must remain compatible across versions.** The new version's Workflow definition must accept the previous version's input; otherwise the new run may fail on its first Workflow Task. +- **Pinned Workflow Types only.** Auto-Upgrade Workflows move at Workflow Task boundaries already; the upgrade-on-CaN pattern adds nothing for them. + ## Best Practices 1. **Check for open executions** before removing old code paths diff --git a/plugins/temporal/skills/temporal-developer/references/python/workflow-streams.md b/plugins/temporal/skills/temporal-developer/references/python/workflow-streams.md index 84ecbd7..b53915b 100644 --- a/plugins/temporal/skills/temporal-developer/references/python/workflow-streams.md +++ b/plugins/temporal/skills/temporal-developer/references/python/workflow-streams.md @@ -11,7 +11,6 @@ Use it for modest fan-out progress streaming: AI-agent runs, order pipelines, mu Only available in the Python SDK today; cross-language is on the roadmap. - ## When to use / not to use - Use it for: updating a UI as an AI agent works; surfacing status from a payment or order pipeline; reporting intermediate results from a data job. diff --git a/plugins/temporal/skills/temporal-developer/references/ruby/gotchas.md b/plugins/temporal/skills/temporal-developer/references/ruby/gotchas.md index c2e962c..e56391d 100644 --- a/plugins/temporal/skills/temporal-developer/references/ruby/gotchas.md +++ b/plugins/temporal/skills/temporal-developer/references/ruby/gotchas.md @@ -59,7 +59,6 @@ require_relative 'activities/my_activity' Transient network errors should be retried. Authentication errors should not be. See `references/ruby/error-handling.md` to understand how to classify errors with `non_retryable: true` and `non_retryable_error_types`. - ## Heartbeating ### Forgetting to Heartbeat Long Activities diff --git a/plugins/temporal/skills/temporal-developer/references/ruby/ruby.md b/plugins/temporal/skills/temporal-developer/references/ruby/ruby.md index bc7f9e0..cf771ea 100644 --- a/plugins/temporal/skills/temporal-developer/references/ruby/ruby.md +++ b/plugins/temporal/skills/temporal-developer/references/ruby/ruby.md @@ -37,13 +37,15 @@ end **worker.rb** - Worker setup (imports activity and workflow, runs indefinitely and processes tasks): ```ruby require 'temporalio/client' +require 'temporalio/env_config' require 'temporalio/worker' require_relative 'say_hello_activity' require_relative 'say_hello_workflow' -# Create client connected to server at the given address -# This is the default port for `temporal server start-dev` -client = Temporalio::Client.connect('localhost:7233', 'default') +args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options +args[0] ||= 'localhost:7233' +args[1] ||= 'default' +client = Temporalio::Client.connect(*args, **kwargs) # Create and run the worker worker = Temporalio::Worker.new( @@ -62,11 +64,14 @@ worker.run **execute_workflow.rb** - Start a workflow execution: ```ruby require 'temporalio/client' +require 'temporalio/env_config' require 'securerandom' require_relative 'say_hello_workflow' -# Create client connected to server at the given address -client = Temporalio::Client.connect('localhost:7233', 'default') +args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options +args[0] ||= 'localhost:7233' +args[1] ||= 'default' +client = Temporalio::Client.connect(*args, **kwargs) # Execute a workflow result = client.execute_workflow( @@ -96,7 +101,7 @@ puts "Result: #{result}" - Can access `Temporalio::Activity::Context.current` for heartbeating ### Worker Setup -- Connect client with `Temporalio::Client.connect` +- Load connection settings with `Temporalio::EnvConfig::ClientConfig.load_client_connect_options` and connect with `Temporalio::Client.connect` - Create worker with `Temporalio::Worker.new(client:, task_queue:, workflows:, activities:)` - Run with `worker.run` diff --git a/plugins/temporal/skills/temporal-developer/references/rust/rust.md b/plugins/temporal/skills/temporal-developer/references/rust/rust.md new file mode 100644 index 0000000..f94c59e --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/rust/rust.md @@ -0,0 +1,179 @@ +# Temporal Rust SDK Reference + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +## Overview + +The Temporal Rust SDK (`temporalio-sdk`) provides native Rust APIs for Workflows, Activities, Workers, and Clients. The SDK is in Public Preview and under active development, so verify exact crate versions and method names against the official docs before giving precise implementation guidance. + +Rust Workflows are structs with macro-decorated methods. Activities are async methods on an `impl` block. Workers register Workflow and Activity types, then poll a Task Queue. + +## Official References + +- [Rust SDK developer guide](https://docs.temporal.io/develop/rust) - Rust documentation hub. +- [Rust SDK Quickstart](https://docs.temporal.io/develop/rust/quickstart) - setup, dependencies, local dev server, and a complete hello-world example. +- [Workflow basics](https://docs.temporal.io/develop/rust/workflows/basics) - Workflow structs, `#[run]`, optional `#[init]`, and message handlers. +- [Activity basics](https://docs.temporal.io/develop/rust/activities/basics) - Activity macros, parameters, and Activity boundaries. +- [Worker processes](https://docs.temporal.io/develop/rust/workers/worker-process) - Worker setup, registration, and Task Queue polling. +- [Temporal Client](https://docs.temporal.io/develop/rust/client/temporal-client) - connecting to Temporal Service, starting Workflows, and fetching results. +- [docs.rs temporalio-sdk](https://docs.rs/temporalio-sdk/latest/temporalio_sdk/) - generated Rust API documentation. +- [sdk-rust examples](https://github.com/temporalio/sdk-rust/tree/main/crates/sdk/examples) - current example programs from the SDK repository. + +## Quick Demo of Temporal + +**Add dependencies:** Follow the [official Rust SDK Quickstart](https://docs.temporal.io/develop/rust/quickstart) for the current `Cargo.toml` dependencies. + +**src/activities.rs** - Activity definition: + +```rust +use temporalio_macros::activities; +use temporalio_sdk::activities::{ActivityContext, ActivityError}; + +pub struct MyActivities; + +#[activities] +impl MyActivities { + #[activity] + pub async fn greet(_ctx: ActivityContext, name: String) -> Result { + Ok(format!("Hello, {}!", name)) + } +} +``` + +**src/workflows.rs** - Workflow definition: + +```rust +use temporalio_macros::{workflow, workflow_methods}; +use temporalio_sdk::{ActivityOptions, WorkflowContext, WorkflowContextView, WorkflowResult}; +use std::time::Duration; + +use crate::activities::MyActivities; + +#[workflow] +pub struct GreetingWorkflow { + name: String, +} + +#[workflow_methods] +impl GreetingWorkflow { + #[init] + fn new(_ctx: &WorkflowContextView, name: String) -> Self { + Self { name } + } + + #[run] + pub async fn run(ctx: &mut WorkflowContext) -> WorkflowResult { + let name = ctx.state(|s| s.name.clone()); + + // Execute an activity + let greeting = ctx.start_activity( + MyActivities::greet, + name, + ActivityOptions::start_to_close_timeout(Duration::from_secs(30)), + ).await?; + + println!("{}", greeting); + Ok(greeting) + } +} +``` + +**src/main.rs** - Worker setup: + +```rust +use temporalio_client::{Client, ClientOptions, Connection}; +use temporalio_common::envconfig::LoadClientConfigProfileOptions; +use temporalio_sdk::{Worker, WorkerOptions}; +use temporalio_sdk_core::{CoreRuntime, RuntimeOptions}; + +mod workflows; +mod activities; + +use crate::workflows::GreetingWorkflow; +use crate::activities::MyActivities; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let runtime = CoreRuntime::new_assume_tokio(RuntimeOptions::builder().build()?)?; + + // Set up client connection options, loading from config if available + let (connection_options, client_options) = ClientOptions::load_from_config( + LoadClientConfigProfileOptions::default(), + )?; + + let connection = Connection::connect(connection_options).await?; + let client = Client::new(connection, client_options)?; + + let worker_options = WorkerOptions::new("my-task-queue") + .register_activities(MyActivities) + .register_workflow::() + .build(); + + Worker::new(&runtime, client, worker_options)?.run().await?; + + Ok(()) +} +``` + +**Run locally:** + +1. Start the dev server with `temporal server start-dev`. +2. Run the Worker with `cargo run`. +3. Start a Workflow Execution with the CLI: + +```sh +temporal workflow start \ + --type GreetingWorkflow \ + --task-queue my-task-queue \ + --input '"Ziggy"' +``` + +## Key Concepts + +### Workflow Definition + +- Define a struct and annotate it with `#[workflow]`. +- Put Workflow methods in a `#[workflow_methods]` impl block. +- Use `#[run]` for the main Workflow logic, and optionally use `#[init]`, `#[signal]`, `#[query]`, and `#[update]`. + +### Activity Definition + +- Put Activity methods in a `#[activities]` impl block. +- Annotate each Activity method with `#[activity]`. +- Activities can perform I/O, call services, use system time, and do other non-deterministic work. + +### Worker Setup + +- A Worker registers Workflow and Activity types, then polls one Task Queue. +- Workers polling the same Task Queue should register the same Workflow and Activity types. +- Keep Worker runtime, client, config, secrets, and logging setup outside Workflow code. + +### Temporal Client + +- Use the Rust client outside Workflow code to start Workflows and send Signals, Queries, and Updates. +- Do not create or use a Temporal Client inside Workflow code. +- A Client can be used inside an Activity when the Activity needs to interact with Temporal Service. + +## File Organization Best Practice + +Keep Workflow definitions, Activity implementations, Worker setup, and starter/client code separate. This makes the determinism boundary easy to inspect. + +```text +my_temporal_app/ +|-- src/ +| |-- activities.rs # Activity implementations and side effects +| |-- workflows.rs # Workflow definitions and orchestration +| `-- main.rs # Worker process in the Quickstart +`-- Cargo.toml +``` + +## Common Pitfalls + +1. **Calling I/O from a Workflow** - Put network, database, filesystem, process calls, and other side effects in Activities. +2. **Mixing Worker and Workflow concerns** - Runtime setup, clients, secrets, environment config, and external logging sinks belong outside Workflow code. +3. **Assuming APIs are stable** - The Rust SDK is Public Preview, so check official docs, docs.rs, and SDK examples before naming exact APIs. + +## Rust-Specific References Status + +Rust-specific local reference files do not exist yet. For deeper Rust SDK details, use the official Rust SDK docs, docs.rs, and [`sdk-rust` examples](https://github.com/temporalio/sdk-rust/tree/main/crates/sdk/examples). For SDK-neutral Temporal concepts, use the core references under `references/core/`. diff --git a/plugins/temporal/skills/temporal-developer/references/typescript/integrations/braintrust.md b/plugins/temporal/skills/temporal-developer/references/typescript/integrations/braintrust.md new file mode 100644 index 0000000..7e60cd8 --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/typescript/integrations/braintrust.md @@ -0,0 +1,81 @@ +# Temporal Braintrust Integration (TypeScript) + +## Overview + +[Braintrust](https://braintrust.dev) is an LLM observability and prompt-management platform. The Temporal TypeScript integration is delivered as the `@braintrust/temporal` package, which exposes a `BraintrustTemporalPlugin` that registers on both the Temporal Client and the Worker. Once registered, the plugin produces Braintrust spans for Workflow and Activity executions and propagates trace context across the Worker boundary. + +The Temporal TypeScript documentation lists Braintrust as a supported integration and points to the Braintrust-hosted guide as the canonical reference. + +> Canonical TypeScript guide: . Treat the Braintrust-hosted page as authoritative for TypeScript-specific API surface; this reference file captures only what is independently verifiable from Temporal's documentation and the canonical guide. + +For conceptual LLM patterns shared across SDKs read `references/core/ai-patterns.md`. + +## Prerequisites + +- An existing Temporal TypeScript development environment as described in `references/typescript/typescript.md`. +- Temporal TypeScript SDK 2.1.0 or later. +- A Braintrust account. + +## Install + +```bash +npm install @braintrust/temporal braintrust @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity @temporalio/common +``` + +The integration package is `@braintrust/temporal`; it sits alongside the standard `braintrust` SDK and the relevant `@temporalio/*` packages. + +## Initialize the Braintrust logger + +Initialize the Braintrust logger before constructing the Temporal Client and Worker so spans connect to the active project. + +```typescript +import * as braintrust from "braintrust"; + +braintrust.initLogger({ projectName: "my-project" }); +``` + +## Register `BraintrustTemporalPlugin` on the Client and the Worker + +Create one `BraintrustTemporalPlugin` instance and pass it to **both** the Client and the Worker via `plugins`. + +```typescript +import { Client, Connection } from "@temporalio/client"; +import { Worker } from "@temporalio/worker"; +import { BraintrustTemporalPlugin } from "@braintrust/temporal"; +import * as activities from "./activities"; + +const plugin = new BraintrustTemporalPlugin(); + +const client = new Client({ + connection: await Connection.connect(), + plugins: [plugin], +}); + +const worker = await Worker.create({ + taskQueue: "my-task-queue", + workflowsPath: require.resolve("./workflows"), + activities, + plugins: [plugin], +}); +``` + +The Client registration links client-initiated spans to the Workflow Executions they start. The Worker registration produces the Workflow and Activity spans inside Braintrust. + +## What Braintrust traces + +The plugin captures: + +- Workflow execution spans named `temporal.workflow.`, including Workflow type, ID, run ID, and errors. +- Activity execution spans named `temporal.activity.`, including Activity type, ID, result, errors, and parent Workflow metadata. +- Trace context propagated through Temporal headers to Activities, Local Activities, and Child Workflows. +- Parent-child relationships across Client calls, Workflows, and Activities. + +## Common mistakes + +- **Initializing the Braintrust logger after constructing the Client or Worker.** Call `braintrust.initLogger({ projectName: ... })` first so the Worker process attaches spans to the correct project. +- **Registering `BraintrustTemporalPlugin` on only one side.** Register on both the Client and the Worker so client-side spans link to the Workflows they start. + +## Additional Resources + +- Canonical TypeScript guide: . +- `references/core/ai-patterns.md` — conceptual LLM patterns shared across SDKs. diff --git a/plugins/temporal/skills/temporal-developer/references/typescript/integrations/mastra.md b/plugins/temporal/skills/temporal-developer/references/typescript/integrations/mastra.md new file mode 100644 index 0000000..99ec9e6 --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/typescript/integrations/mastra.md @@ -0,0 +1,199 @@ +# Temporal Mastra Integration (TypeScript) + +## Overview + +[Mastra](https://mastra.ai/docs) is a TypeScript agent / workflow framework. The `@mastra/temporal` package transforms Mastra workflow and step definitions into Temporal Workflows and Activities at build time, then auto-registers them on a Temporal Worker via the `MastraPlugin`. Each `createStep` becomes a Temporal Activity and each `createWorkflow` becomes a Temporal Workflow. + +Mastra appears on the Temporal TypeScript integrations page as the "Mastra | Agent framework" row, which links out to the upstream Mastra deployment guide. + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. The upstream `@mastra/temporal` package is also flagged as "experimental and not ready for production use"; the API may change between releases. + +For Temporal TypeScript SDK fundamentals (Worker, Workflow, Activity, Task Queue, replay), see `references/typescript/typescript.md` and `references/typescript/determinism.md`. + +## Install + +```bash +npm install @mastra/temporal@latest @temporalio/client @temporalio/worker @temporalio/envconfig +``` + +`pnpm`, `yarn`, and `bun` equivalents are all supported. + +## Initialize the integration + +Wire up a Temporal `Client` once at module scope and pass it to `init()` from `@mastra/temporal`. `init()` returns Mastra's `createWorkflow` and `createStep` factories bound to that client and task queue. + +```ts +// src/temporal.ts +import { init } from '@mastra/temporal' +import { Client, Connection } from '@temporalio/client' +import { loadClientConnectConfig } from '@temporalio/envconfig' + +const config = loadClientConnectConfig() +const connection = await Connection.connect(config.connectionOptions) +const client = new Client({ connection }) + +export const { createWorkflow, createStep } = init({ + client, + taskQueue: 'mastra', +}) +``` + +`init()` parameters: + +- `client` — a `@temporalio/client` `Client` instance. +- `taskQueue` — the Task Queue name Mastra-derived Workflows and Activities run on. The same value must be passed to the Worker (below). +- `startToCloseTimeout` — optional. Maximum activity runtime. **Default: 1 minute.** Accepts string values like `'5 minutes'`. + +`loadClientConnectConfig()` from `@temporalio/envconfig` reads `TEMPORAL_ADDRESS`, `TEMPORAL_NAMESPACE`, and `TEMPORAL_API_KEY` from the environment. + +## Define a step and a workflow + +Use the bound `createStep` and `createWorkflow` from `src/temporal.ts`. Each `createStep` becomes a Temporal Activity; each `createWorkflow` becomes a Temporal Workflow. + +```ts +// src/mastra/workflows.ts +import { z } from 'zod' +import { createWorkflow, createStep } from '../temporal' + +const incrementStep = createStep({ + id: 'increment', + inputSchema: z.object({ + value: z.number(), + }), + outputSchema: z.object({ + value: z.number(), + }), + execute: async ({ inputData }) => { + return { value: inputData.value + 1 } + }, +}) + +const workflow = createWorkflow({ + id: 'increment-workflow', + steps: [incrementStep], + inputSchema: z.object({ + value: z.number(), + }), + outputSchema: z.object({ + value: z.number(), + }), +}).then(incrementStep) + +workflow.commit() + +export { workflow as incrementWorkflow } +``` + +- **Workflow `id` must be a static string literal.** The build-time transformer derives each Workflow's Temporal export name from this `id`, so it cannot be a variable, template, or computed value. +- **Call `workflow.commit()` before exporting.** Workflows without `.commit()` are not picked up by the build-time transformer. + +## Register workflows with Mastra + +```ts +// src/mastra/index.ts +import { Mastra } from '@mastra/core' +import { PinoLogger } from '@mastra/loggers' +import { incrementWorkflow } from './workflows' + +export const mastra = new Mastra({ + workflows: { incrementWorkflow }, + logger: new PinoLogger({ name: 'Mastra', level: 'info' }), +}) +``` + +## Worker + +Construct a `MastraPlugin` from `@mastra/temporal/worker`, run its build-time `prebuild` step pointing at the Mastra entry file, then pass the plugin into `Worker.create({ plugins: [...] })`. + +```ts +// src/mastra/worker.ts +import { MastraPlugin } from '@mastra/temporal/worker' +import { NativeConnection, Worker } from '@temporalio/worker' + +const connection = await NativeConnection.connect({ + address: 'localhost:7233', +}) + +const mastraPlugin = new MastraPlugin() + +await mastraPlugin.prebuild({ + entryFile: import.meta.resolve('./index.ts'), +}) + +const worker = await Worker.create({ + connection, + namespace: 'default', + taskQueue: 'mastra', + plugins: [mastraPlugin], +}) + +await worker.run() +``` + +- **Don't pass `activities` to `Worker.create`.** `MastraPlugin` auto-registers every Activity derived from `createStep` after `prebuild` runs. +- **`taskQueue` on the Worker must match the `taskQueue` passed to `init()`.** Both sides target the same queue. +- **`prebuild({ entryFile })` is a build-time transform.** It must complete before `Worker.create`; pass the path to the Mastra entry file (`src/mastra/index.ts` above). + +## Run a workflow + +Resolve the workflow off the configured `mastra` instance and start a run. The bound client routes execution through Temporal. + +```ts +// scripts/run.ts +import { mastra } from '../src/mastra' + +const run = await mastra.getWorkflow('incrementWorkflow').createRun() +const result = await run.start({ inputData: { value: 5 } }) + +console.log(result) +``` + +## Local development + +```bash +docker run --rm -p 7233:7233 -p 8080:8080 temporalio/auto-setup:latest +``` + +Run the worker in another terminal: + +```bash +npx tsx src/mastra/worker.ts +``` + +The Temporal UI is available at `http://localhost:8080`. + +## Environment variables + +`@temporalio/envconfig`'s `loadClientConnectConfig()` consumes these variables when wiring the Client connection: + +- `TEMPORAL_ADDRESS` +- `TEMPORAL_NAMESPACE` +- `TEMPORAL_API_KEY` + +## Hard constraints + +- **Workflow `id` must be a static string literal.** Pass a literal to `createWorkflow({ id: 'my-workflow', ... })`; the build-time transformer derives the Temporal export name from it. +- **Don't pass `activities` to `Worker.create`.** `MastraPlugin` auto-registers Activities; manual registration conflicts with the plugin. +- **`mastraPlugin.prebuild({ entryFile })` must run before `Worker.create`.** The transform produces the Workflow and Activity definitions the Worker hosts. +- **Temporal Workers require a long-lived process.** Don't deploy the worker to serverless platforms that hibernate between requests. + +## Common mistakes + +- Importing `MastraPlugin` from `@mastra/temporal` instead of `@mastra/temporal/worker`. +- Passing `activities` to `Worker.create` alongside `MastraPlugin`. +- Forgetting `workflow.commit()` after `.then(step)` — the transformer skips uncommitted workflows. +- Using a computed `id` on `createWorkflow` — breaks the build-time transformer's Temporal export naming. +- Mismatching `taskQueue` between `init()` and `Worker.create`. +- Calling `MastraPlugin` without first running `prebuild({ entryFile })`. + +## Out of scope + +The upstream Mastra Temporal guide covers `createWorkflow` and `createStep` only. Mastra Agents, Tools, Memory, RAG, evals, and Mastra Studio are **not** documented as participating in this Temporal integration. + +For language-agnostic AI/LLM orchestration patterns (centralized retries, tool placement, multi-agent), see `references/core/ai-patterns.md`. + +## Resources + +- Temporal TypeScript integrations index: +- Upstream Mastra deployment guide: diff --git a/plugins/temporal/skills/temporal-developer/references/typescript/integrations/opentelemetry.md b/plugins/temporal/skills/temporal-developer/references/typescript/integrations/opentelemetry.md new file mode 100644 index 0000000..c11afb2 --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/typescript/integrations/opentelemetry.md @@ -0,0 +1,77 @@ +# Temporal OpenTelemetry Integration (TypeScript) + +## Overview + +`@temporalio/interceptors-opentelemetry` wires OpenTelemetry tracing into Temporal through the `OpenTelemetryPlugin`. It traces Client, Workflow, Activity, and Nexus code, propagating W3C TraceContext + Baggage across all of them. + +Workflow-side spans are emitted out of the Workflow isolate through an injected Sink that hands serialized spans to a host-side `SpanProcessor`. + +For observability beyond OpenTelemetry tracing (metrics, runtime logger, sinks) read `references/typescript/observability.md`. + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +## Install the plugin + +Install `@temporalio/interceptors-opentelemetry` plus the OpenTelemetry peer packages you use — typically `@opentelemetry/api`, `@opentelemetry/sdk-trace-base`, and `@opentelemetry/resources` (plus an exporter package such as `@opentelemetry/exporter-trace-otlp-grpc` when you ship spans to a collector). + +## `OpenTelemetryPlugin` + +Construct one `OpenTelemetryPlugin` and pass it to the Client, `bundleWorkflowCode`, and `Worker.create`. It must reach `bundleWorkflowCode` so the Workflow-side interceptors are included in the bundle. Lifecycle spans (workflow / activity / client / nexus) are then created automatically. + +```ts +import { Resource } from '@opentelemetry/resources'; +import { BasicTracerProvider, ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; +import { NativeConnection, Worker, bundleWorkflowCode } from '@temporalio/worker'; +import { OpenTelemetryPlugin } from '@temporalio/interceptors-opentelemetry'; + +const resource = new Resource({ 'service.name': 'orders-worker' }); +const spanProcessor = new SimpleSpanProcessor(new ConsoleSpanExporter()); // swap in your own exporter + +const provider = new BasicTracerProvider({ resource }); +provider.addSpanProcessor(spanProcessor); +provider.register(); + +// `resource` and `spanProcessor` are required; pass an optional `tracer` to override +// the tracer used by the Client/Activity interceptors. +const plugin = new OpenTelemetryPlugin({ resource, spanProcessor }); + +const bundle = await bundleWorkflowCode({ + workflowsPath: require.resolve('./workflows'), + plugins: [plugin], +}); + +const connection = await NativeConnection.connect(); +const worker = await Worker.create({ + connection, + taskQueue: 'orders', + workflowBundle: bundle, + activities: { /* ... */ }, + plugins: [plugin], +}); +await worker.run(); +``` + +Pass the same plugin to the Client so client-side calls are traced: + +```ts +import { Client, Connection } from '@temporalio/client'; + +const client = new Client({ + connection: await Connection.connect(), + plugins: [plugin], +}); +``` + +The SDK uses the global OpenTelemetry propagator (default: W3C TraceContext + Baggage). To use a non-default propagator (e.g. Jaeger), call `propagation.setGlobalPropagator(...)` at the top level of your Workflow code BEFORE the Worker bundles it. + +## Common mistakes + +- **Passing only `resource` or only `spanProcessor`.** Both are required; `new OpenTelemetryPlugin()` with no argument throws. +- **Passing the plugin to `Worker.create` but not `bundleWorkflowCode`.** Workflow-side interceptors must be in the bundle. +- **Installing `@temporalio/opentelemetry`.** The package is `@temporalio/interceptors-opentelemetry`. +- **Expecting a non-default propagator (e.g. Jaeger) to work without setting the global propagator before `bundleWorkflowCode` runs.** + +## Resources + +- SDK metrics / observability reference: `references/typescript/observability.md` diff --git a/plugins/temporal/skills/temporal-developer/references/typescript/integrations/vercel-ai-sdk.md b/plugins/temporal/skills/temporal-developer/references/typescript/integrations/vercel-ai-sdk.md new file mode 100644 index 0000000..7c7b968 --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/typescript/integrations/vercel-ai-sdk.md @@ -0,0 +1,191 @@ +# Temporal Vercel AI SDK Integration (TypeScript) + +## Overview + +`@temporalio/ai-sdk` is the Temporal TypeScript SDK integration for [Vercel's AI SDK](https://ai-sdk.dev/) v7. It registers an `AiSdkPlugin` on the Worker so that LLM calls made by functions like `generateText()`, along with MCP tool invocations, run as Temporal Activities under Temporal's retry, timeout, and Durable Execution semantics. AI SDK tool functions execute inside the Workflow and must delegate any non-deterministic work to Activities, while the Workflow author otherwise writes normal AI SDK code. + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +For cross-SDK AI/LLM patterns (Activities wrapping LLM calls, centralized retries, multi-agent orchestration) see `references/core/ai-patterns.md`. For TypeScript SDK fundamentals (Worker setup, `proxyActivities`, the V8 workflow sandbox) see `references/typescript/typescript.md` and `references/typescript/determinism.md` — this file does not restate them. + +## Prerequisites + +- The standard TypeScript SDK setup from `references/typescript/typescript.md` (Temporal CLI installed, `@temporalio/client`, `@temporalio/worker`, `@temporalio/workflow`, `@temporalio/activity`). +- Familiarity with the Vercel AI SDK itself — for AI SDK API details refer to the [Vercel AI SDK documentation](https://ai-sdk.dev/). +- Provider credentials available to the Worker process. Most AI SDK providers read credentials from environment variables; the client process does **not** need provider credentials. + +## Install + +```bash +npm install @temporalio/ai-sdk +``` + +## Configure the Worker + +Register `AiSdkPlugin` on `Worker.create` and pass a `modelProvider` (any AI SDK provider, e.g. `openai` from `@ai-sdk/openai`). The provider is what creates models when the workflow calls `temporalProvider.languageModel('')`. + +```ts +import { openai } from '@ai-sdk/openai'; +import { AiSdkPlugin } from '@temporalio/ai-sdk'; +import { Worker } from '@temporalio/worker'; +import * as activities from './activities'; + +const worker = await Worker.create({ + plugins: [ + new AiSdkPlugin({ + modelProvider: openai, + }), + ], + namespace: 'default', + taskQueue: 'ai-sdk', + workflowsPath: require.resolve('./workflows'), + activities, +}); +``` + +Make sure the Client and Worker share the same Task Queue and Namespace. + +## Use the AI SDK inside a Workflow + +In Workflow code, call AI SDK functions exactly as you would outside Temporal, but pass `temporalProvider.languageModel('')` as `model`. The string is forwarded to the configured `modelProvider` to construct the model; the call itself runs as a Temporal Activity. + +```ts +import { generateText } from 'ai'; +import { temporalProvider } from '@temporalio/ai-sdk/workflow'; + +export async function haikuAgent(prompt: string): Promise { + const result = await generateText({ + model: temporalProvider.languageModel('gpt-4o-mini'), + prompt, + system: 'You only respond in haikus.', + }); + return result.text; +} +``` + +The workflow now inherits Durable Execution: automatic retries on the LLM Activity, configurable timeouts, and recovery across Worker crashes. + +## Tools + +The AI SDK lets the model call tools; with this plugin, tool functions execute inside the Workflow. Because Workflow code must stay deterministic, any tool that performs I/O must delegate to an Activity. Obtain the Activity through `proxyActivities` and use it as the tool's `execute`. + +Activity (regular Temporal Activity in `activities.ts`): + +```ts +export async function getWeather(input: { + location: string; +}): Promise<{ city: string; temperatureRange: string; conditions: string }> { + return { + city: input.location, + temperatureRange: '14-20C', + conditions: 'Sunny with wind.', + }; +} +``` + +Workflow that exposes the Activity as a tool: + +```ts +import { proxyActivities } from '@temporalio/workflow'; +import { generateText, tool } from 'ai'; +import { temporalProvider } from '@temporalio/ai-sdk/workflow'; +import { z } from 'zod'; +import type * as activities from './activities'; + +const { getWeather } = proxyActivities({ + startToCloseTimeout: '1 minute', +}); + +export async function toolsAgent(question: string): Promise { + const result = await generateText({ + model: temporalProvider.languageModel('gpt-4o-mini'), + prompt: question, + system: 'You are a helpful agent.', + tools: { + getWeather: tool({ + description: 'Get the weather for a given city', + inputSchema: z.object({ + location: z.string().describe('The location to get the weather for'), + }), + execute: getWeather, + }), + }, + stopWhen: stepCountIs(5), + }); + return result.text; +} +``` + +## Model Context Protocol (MCP) servers + +The plugin ships a stateless MCP client that runs inside a Workflow. Calls to MCP servers (listing tools, invoking them) run as Activities behind the scenes, so retries, timeouts, and observability come from Temporal. + +### 1. Register MCP client factories on the Worker + +Build a `mcpClientFactories` map keyed by server name. Each factory returns an MCP client built with `experimental_createMCPClient` from `@ai-sdk/mcp` (aliased as `createMCPClient` in the example) and a transport from the upstream MCP SDK — e.g. `StdioClientTransport` from `@modelcontextprotocol/sdk/client/stdio.js`. Pass the map to `AiSdkPlugin` via `mcpClientFactories`. Multiple servers can be registered by adding more factory entries. + +```ts +import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + +const mcpClientFactories = { + testServer: () => + createMCPClient({ + transport: new StdioClientTransport({ + command: 'node', + args: ['lib/mcp-server.js'], + }), + }), +}; + +const worker = await Worker.create({ + plugins: [ + new AiSdkPlugin({ + modelProvider: openai, + mcpClientFactories, + }), + ], + // ... +}); +``` + +With `StdioClientTransport`, the Worker starts the MCP server process and connects to it on demand whenever a Task needs it. + +### 2. Use the MCP client inside a Workflow + +Inside the workflow, construct `new TemporalMCPClient({ name: '' })` using the same name as the factory key, then call `await mcpClient.tools()` to get the tools to pass to `generateText`. + +```ts +import { TemporalMCPClient, temporalProvider } from '@temporalio/ai-sdk/workflow'; +import { generateText } from 'ai'; + +export async function mcpAgent(prompt: string): Promise { + const mcpClient = new TemporalMCPClient({ name: 'testServer' }); + const tools = await mcpClient.tools(); + const result = await generateText({ + model: temporalProvider.languageModel('gpt-4o-mini'), + prompt, + tools, + system: 'You are a helpful agent, You always use your tools when needed.', + stopWhen: stepCountIs(5), + }); + return result.text; +} +``` + +## Common mistakes + +- **Importing from the wrong package.** `AiSdkPlugin` comes from `@temporalio/ai-sdk`, while Workflow-side helpers such as `temporalProvider` and `TemporalMCPClient` come from `@temporalio/ai-sdk/workflow`. `generateText` and `tool` come from `ai`; `experimental_createMCPClient` comes from `@ai-sdk/mcp`. +- **Calling `fetch` (or any I/O) directly inside a tool's `execute`.** Tool functions run in the Workflow sandbox and must delegate to an Activity obtained through `proxyActivities`. +- **Passing an option other than `modelProvider`/`mcpClientFactories` to `AiSdkPlugin`.** Only those two options are documented. +- **Constructing `TemporalMCPClient` positionally.** Use the object form `new TemporalMCPClient({ name: '' })`. +- **Mismatched Task Queue or Namespace between Client and Worker.** Both sides must agree, or the Worker will not pick up the workflow. +- **Putting provider credentials on the Client.** Only the Worker process needs provider API keys. + +## Additional Resources + +- [AI SDK by Vercel integration guide](https://docs.temporal.io/develop/typescript/integrations/ai-sdk) — the canonical Temporal doc this reference is grounded in. +- [Vercel AI SDK documentation](https://ai-sdk.dev/) — upstream AI SDK reference, including the provider list at [`ai-sdk.dev/providers/ai-sdk-providers`](https://ai-sdk.dev/providers/ai-sdk-providers). +- `references/core/ai-patterns.md` — cross-SDK AI/LLM patterns. +- `references/typescript/typescript.md` — TypeScript SDK fundamentals. diff --git a/plugins/temporal/skills/temporal-developer/references/typescript/observability.md b/plugins/temporal/skills/temporal-developer/references/typescript/observability.md index 211fbc6..d5c8a77 100644 --- a/plugins/temporal/skills/temporal-developer/references/typescript/observability.md +++ b/plugins/temporal/skills/temporal-developer/references/typescript/observability.md @@ -2,7 +2,9 @@ ## Overview -The TypeScript SDK provides replay-aware logging, metrics, and integrations for production observability. +The TypeScript SDK provides replay-aware logging, metrics, and distributed tracing (OpenTelemetry) for production observability. + +These pillars are complementary: **logging** (below) captures discrete events, **metrics** capture aggregate worker health, **tracing** stitches a single request across Client/Workflow/Activity/Nexus boundaries, and **Search Attributes** make executions queryable. ## Replay-Aware Logging @@ -100,6 +102,10 @@ Runtime.install({ }); ``` +## Distributed Tracing (OpenTelemetry) + +See `references/typescript/integrations/opentelemetry.md`. + ## Search Attributes (Visibility) See the Search Attributes section of `references/typescript/data-handling.md` @@ -111,3 +117,4 @@ See the Search Attributes section of `references/typescript/data-handling.md` 3. Configure Winston or similar for production log aggregation 4. Monitor Prometheus metrics for worker health 5. Use Event History for debugging workflow issues +6. Use the `OpenTelemetryPlugin` for distributed tracing across Client/Workflow/Activity/Nexus boundaries. diff --git a/plugins/temporal/skills/temporal-developer/references/typescript/standalone-activities.md b/plugins/temporal/skills/temporal-developer/references/typescript/standalone-activities.md new file mode 100644 index 0000000..00328bb --- /dev/null +++ b/plugins/temporal/skills/temporal-developer/references/typescript/standalone-activities.md @@ -0,0 +1,148 @@ +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +## Overview + +Standalone Activities are Activities run independently of any Workflow, started directly from a Temporal Client — useful when you need a single durable, retryable task (job-queue style) and not multi-step orchestration. The same Activity method can be executed both as a Standalone Activity and as a Workflow Activity with no code changes. + +Standalone Activities are conceptually the same across all SDKs. Read the [cross-SDK concept file](references/core/standalone-activities.md) if you have not already, and then see below for the TypeScript SDK specific APIs for calling Standalone Activities. + +## Prerequisites + +- Temporal TypeScript SDK v1.17.0 or higher. +- All `@temporalio/*` packages must be pinned to the same version (heads-up — install/upgrade them together). +- Temporal CLI v1.7.0 or higher — see [Temporal CLI install instructions](references/core/install_cli.md) if needed. Dev server includes Standalone Activities support. +- For production, Temporal Server v1.31.0 or higher (or Temporal Cloud). + +## Hosting Activities on a Worker + +The Activity is defined just as activities normally are in Temporal. Worker registration is also the same. + +```typescript +import { NativeConnection, Worker } from '@temporalio/worker'; +import * as activities from './activities'; +import { loadClientConnectConfig } from '@temporalio/envconfig'; + +async function run() { + const config = loadClientConnectConfig(); + const connection = await NativeConnection.connect(config.connectionOptions); + const worker = await Worker.create({ + connection, + namespace: config.namespace, + taskQueue: 'hello-standalone-activities', + activities, // register whatever your activity(ies) is/are + }); + await worker.run(); +} + +run().catch(console.error); +``` + +## Calling and managing Standalone Activities + +Start and manage Standalone Activities from your application code using the Temporal Client. + +### Do not call from inside a Workflow + +Don't call `client.activity.execute` / `client.activity.start` or any other Standalone Activity APIs from inside a Workflow Definition — use Workflow-side activity invocation (`proxyActivities`) instead. + +### Connect a Client + +The Standalone Activity operations are methods on `client.activity`, where `client` is a connected `Client`. The examples below assume this `client`. + +```typescript +import { Connection, Client } from '@temporalio/client'; +import { loadClientConnectConfig } from '@temporalio/envconfig'; + +const config = loadClientConnectConfig(); +const connection = await Connection.connect(config.connectionOptions); +const client = new Client({ connection, namespace: config.namespace }); +``` + +### Execute (wait for result) + +Use `execute` to durably enqueue the Activity, wait for it to run on a Worker, and return the result. The options require `id`, `taskQueue`, and at least one of `startToCloseTimeout` or `scheduleToCloseTimeout`. + +#### With type checking + +Use when activity definitions are available in this language. Call `client.activity.typed()` to obtain a typed Activity Client interface. Calling `typed` does not create a new Client object — it only adjusts the type annotation of the existing Client. + +```typescript +import * as activities from './activities'; +import { nanoid } from 'nanoid'; + +const activitiesClient = client.activity.typed(); + +const activityOptions = { + taskQueue: 'hello-standalone-activities', + startToCloseTimeout: '10s', +}; + +// In practice, use a meaningful business identifier, like customer or transaction identifier +const activityId = nanoid(); + +const result = await activitiesClient.execute('greet', { + ...activityOptions, + id: activityId, + args: ['World'], +}); +``` + +#### Without type checking + +Use when activity definitions are unavailable in this language (i.e. you can't import them). Call `execute` directly on `client.activity`. + +```typescript +const result = await client.activity.execute('greet', { + ...activityOptions, + id: activityId, + args: [1], +}); +``` + +### Start (do not wait for result) + +Use `activitiesClient.start(...)` (or `client.activity.start(...)` on the untyped interface) to durably enqueue the Activity and get back a handle without waiting for completion. This takes the **exact same arguments as `execute`**. + +```typescript +const handle = await activitiesClient.start(...); +``` + +### Get a handle to an existing Activity execution + +Use `client.activity.getHandle(activityId, runId?)` to attach a handle to a previously started Standalone Activity. Omitting `runId` targets the latest run of that Activity ID. `getHandle` is not available on the typed interface, and the optional type argument constrains the result type but isn't verified. + +```typescript +const newHandle = client.activity.getHandle(activityId); +``` + +### Wait for the result of a handle + +```typescript +const result = await handle.result(); +``` + +Calling `execute` is equivalent to `start` followed by `await handle.result()`. + +### List Standalone Activities + +```typescript +const query = 'TaskQueue="hello-standalone-activities"'; + +for await (const a of client.activity.list(query)) { // returns an AsyncIterable + console.log( + `${a.activityId} | ${a.activityRunId} | ${a.activityType} | ${a.status} | ${a.closeTime?.toISOString()}`, + ); +} +``` + +Only Standalone Activity Executions are returned; Activities running inside Workflows are not included. + +### Count Standalone Activities + +Use `client.activity.count(query)` to count matching executions; this takes the **exact same arguments as `list`**. + +```typescript +const { count } = await client.activity.count(query); +console.log(`Total activities: ${count}`); +``` diff --git a/plugins/temporal/skills/temporal-developer/references/typescript/typescript.md b/plugins/temporal/skills/temporal-developer/references/typescript/typescript.md index 96fc089..1c4ff4f 100644 --- a/plugins/temporal/skills/temporal-developer/references/typescript/typescript.md +++ b/plugins/temporal/skills/temporal-developer/references/typescript/typescript.md @@ -15,7 +15,7 @@ Temporal workflows are durable through history replay. For details on how this w **Add Dependencies:** Install the Temporal SDK packages (use the package manager appropriate for your project): ```bash -npm install @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity +npm install @temporalio/client @temporalio/worker @temporalio/workflow @temporalio/activity @temporalio/envconfig ``` Note: if you are working in production, it is strongly advised to use ~ version constraints, i.e. `npm install ... --save-prefix='~'` if using NPM. @@ -46,11 +46,16 @@ export async function greetingWorkflow(name: string): Promise { **worker.ts** - Worker setup (registers activity and workflow, runs indefinitely and processes tasks): ```typescript -import { Worker } from '@temporalio/worker'; +import { NativeConnection, Worker } from '@temporalio/worker'; +import { loadClientConnectConfig } from '@temporalio/envconfig'; import * as activities from './activities'; async function run() { + const config = loadClientConnectConfig(); + const connection = await NativeConnection.connect(config.connectionOptions); const worker = await Worker.create({ + connection, + namespace: config.namespace, workflowsPath: require.resolve('./workflows'), // For production, use workflowBundle instead activities, taskQueue: 'greeting-queue', @@ -68,12 +73,15 @@ run().catch(console.error); **client.ts** - Start a workflow execution: ```typescript -import { Client } from '@temporalio/client'; +import { Client, Connection } from '@temporalio/client'; +import { loadClientConnectConfig } from '@temporalio/envconfig'; import { greetingWorkflow } from './workflows'; import { v4 as uuid } from 'uuid'; async function run() { - const client = new Client(); + const config = loadClientConnectConfig(); + const connection = await Connection.connect(config.connectionOptions); + const client = new Client({ connection, namespace: config.namespace }); const result = await client.workflow.execute(greetingWorkflow, { workflowId: uuid(), @@ -105,6 +113,8 @@ run().catch(console.error); ### Worker Setup +- Load connection settings with `loadClientConnectConfig()` and pass them to `NativeConnection.connect()` +- Pass `namespace: config.namespace` to `Worker.create()` - `NativeConnection` carries no namespace, and the Worker defaults to `default` without it - Use `Worker.create()` with `workflowsPath` (dev) or `workflowBundle` (production) - see `references/typescript/gotchas.md` - Import activities directly (not via proxy) @@ -181,4 +191,5 @@ See `references/typescript/testing.md` for info on writing tests. - **`references/typescript/advanced-features.md`** - Schedules, worker tuning, and more - **`references/typescript/data-handling.md`** - Data converters, payload encryption, etc. - **`references/typescript/versioning.md`** - Patching API, workflow type versioning, Worker Versioning +- **`references/typescript/standalone-activities.md`** - Standalone Activities: run an Activity directly from a Client without a Workflow (Public Preview). Concept overview at `references/core/standalone-activities.md`. - **`references/typescript/determinism-protection.md`** - V8 sandbox and bundling diff --git a/plugins/temporal/skills/temporal-developer/references/typescript/versioning.md b/plugins/temporal/skills/temporal-developer/references/typescript/versioning.md index b4b8e19..2fdb272 100644 --- a/plugins/temporal/skills/temporal-developer/references/typescript/versioning.md +++ b/plugins/temporal/skills/temporal-developer/references/typescript/versioning.md @@ -148,8 +148,6 @@ After all V1 executions complete, remove the old Workflow function. Worker Versioning allows multiple Worker versions to run simultaneously, routing Workflows to specific versions without code-level patching. Workflows are pinned to the Worker Deployment Version they started on. -> **Note:** Worker Versioning is currently in Public Preview. The legacy Worker Versioning API (before 2025) will be removed from Temporal Server in March 2026. - ### Key Concepts - **Worker Deployment**: A logical name for your application (e.g., "order-service") @@ -204,6 +202,46 @@ Worker Versioning is best suited for: For long-running Workflows, consider combining Worker Versioning with the Patching API, or use Continue-as-New to move Workflows to newer versions. +## Upgrading on Continue-as-New + +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +For long-running Pinned Workflows that use Continue-as-New, detect a new Target Worker Deployment Version on `workflowInfo()` and continue-as-new with `InitialVersioningBehavior.AUTO_UPGRADE` so the new run starts on the Target Version. See `references/core/versioning.md` for the conceptual model. + +### Detecting the Target Version change + +`workflowInfo().targetWorkerDeploymentVersionChanged` is `true` when a new Current or Ramping Version is available for this Workflow's Worker Deployment. The flag is refreshed after each Workflow Task completes. + +Check the flag from code that runs as part of a Workflow Task — for example, before accepting an Update, starting an Activity, or starting a child Workflow. + +### Continue-as-new with upgrade + +When the flag is set, build the Continue-as-New function with `makeContinueAsNewFunc`, passing `initialVersioningBehavior: InitialVersioningBehavior.AUTO_UPGRADE`, so the new run starts on the Target Version of its Worker Deployment. + +```ts +import * as wf from '@temporalio/workflow'; +import { InitialVersioningBehavior } from '@temporalio/common'; + +// At a natural Workflow Task boundary, e.g. before accepting Updates, +// starting Activities, starting child Workflows, etc.: +if (wf.workflowInfo().targetWorkerDeploymentVersionChanged) { + const continueAsNew = wf.makeContinueAsNewFunc({ + initialVersioningBehavior: InitialVersioningBehavior.AUTO_UPGRADE, + }); + await continueAsNew(nextInput); +} +``` + +> [!IMPORTANT] +> Don't busy-poll the flag on a timer. Check it at a natural Workflow Task boundary — before accepting Updates, starting Activities, starting child Workflows, etc. For idle Workflows, send a Signal to wake them so they can check it (see Limitations). + +### Limitations + +- **Lazy moving only — idle Workflows do not upgrade.** Send a Signal to wake an idle Workflow so it can check `targetWorkerDeploymentVersionChanged`. +- **Workflow input must remain compatible across versions.** The new version's Workflow definition must accept the previous version's input; otherwise the new run may fail on its first Workflow Task. +- **Pinned Workflow Types only.** Auto-Upgrade Workflows move at Workflow Task boundaries already; the upgrade-on-CaN pattern adds nothing for them. + ## Best Practices 1. Use descriptive `patchId` names that explain the change