diff --git a/.github/workflows/check-redirects.yml b/.github/workflows/check-redirects.yml new file mode 100644 index 0000000000..499c6fed0b --- /dev/null +++ b/.github/workflows/check-redirects.yml @@ -0,0 +1,30 @@ +name: Check Redirects for Moved Pages + +on: + pull_request: + types: + - opened + - synchronize + - reopened + +permissions: + contents: read + +jobs: + check-redirects: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '20' + + - name: Check for missing redirects + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: node bin/check-redirects-for-moved-pages.js diff --git a/.github/workflows/update-cli-docs.yml b/.github/workflows/update-cli-docs.yml index 7ba742c759..3117c97ce2 100644 --- a/.github/workflows/update-cli-docs.yml +++ b/.github/workflows/update-cli-docs.yml @@ -56,7 +56,7 @@ jobs: - name: Setup Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: - go-version: '1.22' + go-version: '1.26' - name: Generate CLI docs working-directory: cli diff --git a/CONTACT.md b/CONTACT.md index a560e3bbac..a66d8e4134 100644 --- a/CONTACT.md +++ b/CONTACT.md @@ -22,7 +22,7 @@ We’re always happy to help and look forward to hearing from you! We believe strongly in Open Source and encourage contributions to our documentation. If you’d like to contribute or help us improve, these links may help: -- **About the docs**: Learn more about the documentation structure and how we maintain it by visiting the [documentation overview](https://github.com/temporalio/documentation/blob/master/README.md). +- **About the docs**: Learn more about the documentation structure and how we maintain it by visiting the [documentation overview](https://github.com/temporalio/documentation/blob/main/README.md). - **Contributing**: Check out our [Contributing Guidelines](https://github.com/temporalio/documentation/blob/main/CONTRIBUTING.md) for how to get started with contributing to the Temporal documentation or codebase. - **We’re Hiring**: Interested in joining the Temporal team? We’re always looking for passionate individuals. Explore open positions at [Temporal Careers](https://temporal.io/careers). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a76523ed18..bc489b3ad9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,7 +75,7 @@ For small changes like fixing typos, you can edit files directly on GitHub. Once approved, your change goes live! 🎉 -Maintainers and contributors to this project are expected to conduct themselves in a respectful way. See the [CNCF Community Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md) as a reference. +Maintainers and contributors to this project are expected to conduct themselves in a respectful way. See the [CNCF Community Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md) as a reference. This repository and its contents are open-source; individual and commercial use are permitted. diff --git a/bin/check-redirects-for-moved-pages.js b/bin/check-redirects-for-moved-pages.js new file mode 100755 index 0000000000..4bcbcba424 --- /dev/null +++ b/bin/check-redirects-for-moved-pages.js @@ -0,0 +1,184 @@ +#!/usr/bin/env node + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const VERCEL_JSON = path.join(process.cwd(), 'vercel.json'); + +function filePathToUrlPath(filePath) { + let urlPath = filePath + .replace(/^docs\//, '/') + .replace(/\.mdx?$/, '') + .replace(/\/index$/, ''); + + if (urlPath === '') urlPath = '/'; + return urlPath; +} + +function extractFrontMatter(content) { + const match = content.match(/^---\n([\s\S]*?)\n---/); + if (!match) return {}; + const block = match[1]; + const result = {}; + for (const field of ['slug', 'id']) { + const m = block.match(new RegExp(`^${field}:\\s*(.+)$`, 'm')); + if (m) result[field] = m[1].trim(); + } + return result; +} + +function getFileContentAtRef(filePath, ref) { + try { + return execSync(`git show ${ref}:${filePath}`, { encoding: 'utf8' }); + } catch { + return null; + } +} + +function resolveOldUrl(filePath, ref) { + const content = getFileContentAtRef(filePath, ref); + if (content) { + const fm = extractFrontMatter(content); + + // slug takes highest precedence + if (fm.slug) { + if (fm.slug.startsWith('/')) return fm.slug; + const dirUrl = filePathToUrlPath(filePath).replace(/\/[^/]*$/, ''); + return `${dirUrl}/${fm.slug}`; + } + + // id replaces the filename segment in the URL + if (fm.id) { + const dirUrl = filePathToUrlPath(filePath).replace(/\/[^/]*$/, ''); + return `${dirUrl}/${fm.id}`; + } + } + return filePathToUrlPath(filePath); +} + +function getMovedOrDeletedDocFiles(baseSha) { + const mergeBase = execSync(`git merge-base HEAD ${baseSha}`, { + encoding: 'utf8', + }).trim(); + + // -M enables rename detection, --diff-filter=DR shows only deletes and renames + const output = execSync( + `git diff --name-status -M --diff-filter=DR ${mergeBase}..HEAD -- docs/`, + { encoding: 'utf8' }, + ); + + const results = []; + for (const line of output.split('\n').filter((l) => l.trim())) { + const parts = line.split('\t'); + const status = parts[0]; + + if (status === 'D') { + results.push({ type: 'deleted', oldPath: parts[1] }); + } else if (status.startsWith('R')) { + results.push({ type: 'renamed', oldPath: parts[1], newPath: parts[2] }); + } + } + + const files = results.filter((r) => /\.(mdx|md)$/.test(r.oldPath)); + return { files, mergeBase }; +} + +function vercelPatternToRegex(pattern) { + // Convert Vercel redirect patterns like /foo/:path* to a regex. + // Replace named params before escaping so the colons and wildcards are + // consumed first, then escape whatever literal characters remain. + const tokens = []; + const tokenized = pattern + .replace(/:([a-zA-Z]+)\*/g, () => { + tokens.push('.+'); + return `__TOKEN_${tokens.length - 1}__`; + }) + .replace(/:([a-zA-Z]+)/g, () => { + tokens.push('[^/]+'); + return `__TOKEN_${tokens.length - 1}__`; + }); + + let regexStr = tokenized.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + for (let i = 0; i < tokens.length; i++) { + regexStr = regexStr.replace(`__TOKEN_${i}__`, tokens[i]); + } + return new RegExp(`^${regexStr}$`); +} + +function loadRedirects() { + const config = JSON.parse(fs.readFileSync(VERCEL_JSON, 'utf8')); + return (config.redirects || []).map((r) => ({ + source: r.source, + regex: vercelPatternToRegex(r.source), + })); +} + +function findMatchingRedirect(urlPath, redirects) { + return redirects.find((r) => r.regex.test(urlPath)); +} + +function main() { + const BASE_SHA = process.env.BASE_SHA; + if (!BASE_SHA) { + console.error('BASE_SHA environment variable is required.'); + process.exit(1); + } + + const { files: movedFiles, mergeBase } = getMovedOrDeletedDocFiles(BASE_SHA); + + if (movedFiles.length === 0) { + console.log('No docs pages were moved or deleted. Nothing to check.'); + process.exit(0); + } + + const redirects = loadRedirects(); + const missing = []; + + for (const file of movedFiles) { + const oldUrl = resolveOldUrl(file.oldPath, mergeBase); + const match = findMatchingRedirect(oldUrl, redirects); + + if (!match) { + file.oldUrl = oldUrl; + missing.push(file); + } + } + + if (missing.length === 0) { + console.log( + `All ${movedFiles.length} moved/deleted page(s) have redirects.`, + ); + process.exit(0); + } + + console.error('Missing redirects for moved/deleted pages:\n'); + for (const file of missing) { + if (file.type === 'renamed') { + const newUrl = resolveOldUrl(file.newPath, 'HEAD'); + console.error( + ` ${file.oldUrl} -> ${newUrl} (renamed, no redirect found)`, + ); + } else { + console.error(` ${file.oldUrl} (deleted, no redirect found)`); + } + } + + console.error( + `\nAdd redirect entries to vercel.json for the ${missing.length} path(s) above.`, + ); + process.exit(1); +} + +module.exports = { + filePathToUrlPath, + extractFrontMatter, + resolveOldUrl, + vercelPatternToRegex, + findMatchingRedirect, + loadRedirects, +}; + +if (require.main === module) { + main(); +} diff --git a/bin/check-redirects-for-moved-pages.test.js b/bin/check-redirects-for-moved-pages.test.js new file mode 100644 index 0000000000..9d05917c9c --- /dev/null +++ b/bin/check-redirects-for-moved-pages.test.js @@ -0,0 +1,120 @@ +const { describe, it } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const { + filePathToUrlPath, + extractFrontMatter, + resolveOldUrl, + vercelPatternToRegex, + findMatchingRedirect, + loadRedirects, +} = require('./check-redirects-for-moved-pages.js'); + +describe('filePathToUrlPath', () => { + it('strips docs/ prefix and extension', () => { + assert.strictEqual( + filePathToUrlPath('docs/cloud/terraform-provider.mdx'), + '/cloud/terraform-provider', + ); + }); + + it('handles .md extension', () => { + assert.strictEqual(filePathToUrlPath('docs/glossary.md'), '/glossary'); + }); + + it('strips /index from directory pages', () => { + assert.strictEqual(filePathToUrlPath('docs/cloud/index.mdx'), '/cloud'); + }); + + it('handles deeply nested paths', () => { + assert.strictEqual( + filePathToUrlPath('docs/develop/go/best-practices/data-handling/data-conversion.mdx'), + '/develop/go/best-practices/data-handling/data-conversion', + ); + }); +}); + +describe('extractFrontMatter', () => { + it('extracts slug', () => { + const content = '---\nid: foo\nslug: /custom/path\n---\nBody text'; + assert.deepStrictEqual(extractFrontMatter(content), { + id: 'foo', + slug: '/custom/path', + }); + }); + + it('returns empty object when no frontmatter', () => { + assert.deepStrictEqual(extractFrontMatter('Just body text'), {}); + }); + + it('handles frontmatter with only id', () => { + const content = '---\nid: my-page\ntitle: My Page\n---\n'; + assert.deepStrictEqual(extractFrontMatter(content), { id: 'my-page' }); + }); +}); + +describe('resolveOldUrl against real repo pages', () => { + it('file path only (no slug, no id)', () => { + const url = resolveOldUrl('docs/cloud/terraform-provider.mdx', 'HEAD'); + assert.strictEqual(url, '/cloud/terraform-provider'); + }); + + it('absolute slug override', () => { + const url = resolveOldUrl( + 'docs/develop/go/best-practices/data-handling/data-conversion.mdx', + 'HEAD', + ); + assert.strictEqual(url, '/develop/go/data-handling/data-conversion'); + }); + + it('id replacing filename', () => { + const url = resolveOldUrl('docs/develop/go/set-up.mdx', 'HEAD'); + assert.strictEqual(url, '/develop/go/set-up-your-local-go'); + }); + + it('index page', () => { + const url = resolveOldUrl('docs/cloud/index.mdx', 'HEAD'); + assert.strictEqual(url, '/cloud'); + }); +}); + +describe('vercelPatternToRegex', () => { + it('matches wildcard patterns', () => { + const regex = vercelPatternToRegex('/production-deployment/cloud/:path*'); + assert.ok(regex.test('/production-deployment/cloud/terraform-provider')); + assert.ok(regex.test('/production-deployment/cloud/foo/bar')); + assert.ok(!regex.test('/cloud/terraform-provider')); + }); + + it('matches single-segment params', () => { + const regex = vercelPatternToRegex('/dev-guide/:slug'); + assert.ok(regex.test('/dev-guide/hello')); + assert.ok(!regex.test('/dev-guide/hello/world')); + }); + + it('matches exact paths', () => { + const regex = vercelPatternToRegex('/cloud/billing-reports'); + assert.ok(regex.test('/cloud/billing-reports')); + assert.ok(!regex.test('/cloud/billing-reports/extra')); + }); +}); + +describe('findMatchingRedirect', () => { + it('finds a match from real vercel.json redirects', () => { + const redirects = loadRedirects(); + const match = findMatchingRedirect( + '/production-deployment/cloud/terraform-provider', + redirects, + ); + assert.ok(match, 'expected wildcard redirect to match'); + }); + + it('returns undefined for paths with no redirect', () => { + const redirects = loadRedirects(); + const match = findMatchingRedirect( + '/this/path/does/not/exist', + redirects, + ); + assert.strictEqual(match, undefined); + }); +}); diff --git a/docs/cloud/audit-logs.mdx b/docs/cloud/audit-logs.mdx index 75db528ba6..ad80996d7a 100644 --- a/docs/cloud/audit-logs.mdx +++ b/docs/cloud/audit-logs.mdx @@ -19,7 +19,7 @@ tags: - Temporal Cloud --- -Audit Logs is a feature of [Temporal Cloud](/cloud/overview) that provides forensic access information for a variety of operations in the Temporal Cloud control plane. +Audit Logs is a feature of [Temporal Cloud](/cloud/overview) that provides forensic access information for a variety of operations in the Temporal Cloud Control Plane. Audit Logs answers "who, when, and what" questions about Temporal Cloud resources. These answers can help you evaluate the security of your organization, and they can provide information that you need to satisfy audit and compliance requirements. diff --git a/docs/cloud/connectivity/index.mdx b/docs/cloud/connectivity/index.mdx index 1f863ebbc1..2744c921d1 100644 --- a/docs/cloud/connectivity/index.mdx +++ b/docs/cloud/connectivity/index.mdx @@ -85,7 +85,13 @@ A public Connectivity Rule has one optional parameter: - `enable-stable-ips`: When set, Namespaces attached to this rule resolve their Namespace endpoint to a published, fixed set of IP addresses you can allowlist in your firewall. See [Stable IPs](/cloud/connectivity/ip-addresses#stable-ip-addresses) for details. -Only one public Connectivity Rule is allowed per account (see [Permissions and limits](#permissions-and-limits) below). If you already have a public Connectivity Rule and want to turn on Stable IPs, update the existing rule rather than creating a new one — attempting to create a second public rule returns an error. +Only one public Connectivity Rule is allowed per account (see [Permissions and limits](#permissions-and-limits) below). + +:::caution + +Connectivity Rules cannot be updated in place. If you already have a public Connectivity Rule and want to turn on Stable IPs, you must delete the existing public rule, create a new one with `enable-stable-ips` set, and re-attach it to every Namespace that needs it. Attempting to create a second public rule alongside the existing one returns an error. + +::: An AWS PrivateLink (PL) private Connectivity Rule requires: @@ -347,7 +353,7 @@ nc -zv vpce-0123456789abcdef-abc.us-east-1.vpce.amazonaws.com 7233 ## Control plane connectivity -Using the Temporal Cloud [web UI](/web-ui), [Terraform provider](/cloud/terraform-provider), [`tcld` CLI](/cloud/tcld), or [Cloud Ops APIs](/ops) requires network access to the Temporal Cloud control plane. +Using the Temporal Cloud [web UI](/web-ui), [Terraform provider](/cloud/terraform-provider), [`tcld` CLI](/cloud/tcld), or [Cloud Ops APIs](/ops) requires network access to the Temporal Cloud Control Plane. ### Control plane hostnames @@ -357,25 +363,35 @@ Different hostnames are used for different parts of the service. - `web.onboarding.tmprl.cloud` (required for Web UI) - `web.saas-api.tmprl.cloud` (required for Web UI) -### AWS PrivateLink connectivity to Temporal Cloud control plane +### AWS PrivateLink connectivity to Temporal Cloud Control Plane -Temporal Cloud supports [AWS PrivateLink](https://aws.amazon.com/privatelink/) connections to the control plane, which allows access from applications running in VPCs that cannot egress to the public internet. Temporal Cloud does **not** support restricting an account so that private connectivity is the sole connectivity method to the control plane; the control plane is always accessible via public internet. +Temporal Cloud supports [AWS PrivateLink](https://aws.amazon.com/privatelink/) connections to the Control Plane, which allows access from applications running in VPCs that cannot egress to the public internet. Temporal Cloud does **not** support restricting an account so that private connectivity is the sole connectivity method to the Control Plane; the Control Plane is always accessible via public internet. -Control plane access is always securely authenticated via [API keys](/cloud/api-keys#overview) or JWT tokens, regardless of how you choose to connect. +Control Plane access is always securely authenticated via [API keys](/cloud/api-keys#overview) or JWT tokens, regardless of how you choose to connect. -To set up a PrivateLink connection to the Temporal Cloud control plane, follow [these instructions](/cloud/connectivity/aws-connectivity), but use the control plane endpoint information below: +To set up a PrivateLink connection to the Temporal Cloud Control Plane, follow [these instructions](/cloud/connectivity/aws-connectivity), but use the Control Plane endpoint information below: | Hostname | Region | Control Plane PrivateLink Service Name | | ---------------------- | ----------- | --------------------------------------------------------- | | `saas-api.tmprl.cloud` | `us-west-2` | `com.amazonaws.vpce.us-west-2.vpce-svc-0c57a5930b6f6be0e` | -The control plane PrivateLink endpoint includes a [private DNS name](https://docs.aws.amazon.com/vpc/latest/privatelink/manage-dns-names.html), which lets your clients use the PrivateLink connection without having to set up private DNS or having to override client configuration. To use the DNS name, make sure your VPC has the `Enable DNS hostnames` and `Enable DNS support` options enabled. If you cannot use the DNS name, you can also manually [set up private DNS](/cloud/connectivity/aws-connectivity#configuring-private-dns-for-aws-privatelink) or [override the server and TLS settings on your clients](/cloud/connectivity#update-dns-or-clients-to-use-private-connectivity). +The Control Plane PrivateLink endpoint includes a [private DNS name](https://docs.aws.amazon.com/vpc/latest/privatelink/manage-dns-names.html), which lets your clients use the PrivateLink connection without having to set up private DNS or having to override client configuration. To use the DNS name, make sure your VPC has the `Enable DNS hostnames` and `Enable DNS support` options enabled. If you cannot use the DNS name, you can also manually [set up private DNS](/cloud/connectivity/aws-connectivity#configuring-private-dns-for-aws-privatelink) or [override the server and TLS settings on your clients](/cloud/connectivity#update-dns-or-clients-to-use-private-connectivity). -:::caution Finish client setup +:::caution Finish client setup to programmatically access Temporal Cloud Control Plane over PrivateLink -After creating a private connection, you must use the provided DNS name, set up private DNS, or update the configuration of all clients you want to use the private connection. +For Temporal clients to access the Control Plane over AWS PrivateLink, you must either use the provided DNS name, set up private DNS, or update the client configuration, as described above. -Without this step, your clients may connect to the control plane over the internet if they were previously using public connectivity, or they will not be able to connect at all. +Without this step, your clients may connect to the Temporal Cloud Control Plane over the internet, or they may not connect at all. ::: -The control plane is also accessible via the PrivateLink endpoint in AWS us-west-2 that can be used for namespace traffic, but we strongly recommend using the control-plane specific endpoint for control plane traffic. +#### Extend the Control Plane PrivateLink endpoint to other AWS regions with VPC Peering + +The Temporal Cloud Control Plane PrivateLink Service is only exposed in `us-west-2`. To reach the Control Plane privately from another AWS region, set up a us-west-2 VPC with a VPC Endpoint to the Control Plane, then [VPC-Peer](https://docs.aws.amazon.com/vpc/latest/peering/what-is-vpc-peering.html) that VPC to your other-region VPCs. + +To set this up: + +1. Create a VPC in `us-west-2`. +2. Create the [VPC Endpoint](https://docs.aws.amazon.com/vpc/latest/privatelink/concepts.html) to the Control Plane in that us-west-2 VPC, using the service name from the table above. Follow the [standard PrivateLink setup steps](/cloud/connectivity/aws-connectivity), but use the Control Plane service name. +3. Peer the us-west-2 VPC to each VPC in another region that needs Control Plane access. [VPC Peering shares VPC Endpoints](https://docs.aws.amazon.com/vpc/latest/peering/peering-configurations-partial-access.html) across peered VPCs, which gives each peered VPC a private path to the Control Plane. + +Peering is per-VPC, so you must peer the us-west-2 VPC to every VPC that needs Control Plane access. There is no limit on how many regions you can extend to using this approach. diff --git a/docs/cloud/export.mdx b/docs/cloud/export.mdx index c17255c5a1..eb8d403bec 100644 --- a/docs/cloud/export.mdx +++ b/docs/cloud/export.mdx @@ -19,7 +19,7 @@ tags: Workflow History Export allows users to export closed Workflow Histories from Temporal Cloud to cloud object storage (AWS S3 or GCP GCS), enabling: -- Compliance and audit trails of complete Event History data in [proto format](https://github.com/temporalio/api/blob/master/temporal/api/export/v1/message.proto) +- Compliance and audit trails of complete Event History data in [proto format](https://github.com/temporalio/api/blob/main/temporal/api/export/v1/message.proto) - Analytics on Event History when ingested to the data platform of your choice Workflow History Export in Temporal Cloud provides similar functionality as [Archival](/self-hosted-guide/archival) in a Self-Hosted Temporal Server. @@ -31,7 +31,7 @@ Delivery is guaranteed at least once. ## What's in the exported data {/* #exported-data */} -Each exported file contains one or more complete Workflow Execution histories serialized as protocol buffers using the [`WorkflowExecutions`](https://github.com/temporalio/api/blob/master/temporal/api/export/v1/message.proto) proto. +Each exported file contains one or more complete Workflow Execution histories serialized as protocol buffers using the [`WorkflowExecutions`](https://github.com/temporalio/api/blob/main/temporal/api/export/v1/message.proto) proto. Each history is an ordered sequence of events that records everything that happened during a Workflow Execution: @@ -45,7 +45,7 @@ Each history is an ordered sequence of events that records everything that happe Search attributes in the export use your **user-defined names** (for example, `customerId`), not internal column names. The export format is **protobuf binary**. -You must deserialize using the [proto schema](https://github.com/temporalio/api/blob/master/temporal/api/export/v1/message.proto) before the data is human-readable. +You must deserialize using the [proto schema](https://github.com/temporalio/api/blob/main/temporal/api/export/v1/message.proto) before the data is human-readable. The following is a simplified JSON representation of what one Workflow Execution looks like after deserialization. This example shows a Workflow that started, ran one Activity, and completed: @@ -176,15 +176,12 @@ is the time the export uploads to object storage, not the Workflow completion ti - Usage Dashboard: - Actions from the Export Job are included in the [Usage Dashboard](/cloud/actions-usage). -3. **Metrics**: - - Export-related metrics are available from the [Cloud metrics endpoint](/cloud/metrics/), specifically the metric `temporal_cloud_v1_total_action_count` with the label `is_background="true"`. - -4. **Email**: +3. **Email**: - Emails are sent to `Namespace Administrator`, `Account Owner`, and `Global Administrator` roles when a Workflow History Export job fails due to a user related error (such as Object Store permissions issue). ## Working with exported files -Use the proto schema defined [here](https://github.com/temporalio/api/blob/master/temporal/api/export/v1/message.proto) to deserialize exported files. +Use the proto schema defined [here](https://github.com/temporalio/api/blob/main/temporal/api/export/v1/message.proto) to deserialize exported files. ### Using exported files in analytics diff --git a/docs/cloud/get-started/namespaces.mdx b/docs/cloud/get-started/namespaces.mdx index 319ceae6d2..6ef959ec92 100644 --- a/docs/cloud/get-started/namespaces.mdx +++ b/docs/cloud/get-started/namespaces.mdx @@ -215,8 +215,8 @@ To create a Namespace in Temporal Cloud, gather the following information: 1. In **Region**, select the region in which to host this Namespace. 1. In **Retention Period**, specify a value from 1 to 90 days. When choosing this value, consider your needs for Event History versus the cost of maintaining that Event History. Typically, a development Namespace has a short retention - period and a production Namespace has a longer retention period. (If you need to change this value later, contact - [Temporal Support](/cloud/support#support-ticket).) + period and a production Namespace has a longer retention period. The retention period of a namespace can be changed + in the Temporal Cloud UI under the namespace's Settings tab or by using the Temporal CLI. 1. Select your authentication method: [API keys](/cloud/api-keys) or [mTLS](/cloud/certificates). 1. If using mTLS authentication, paste the CA certificate for this Namespace. 1. Optional: In **Codec Server**, enter the HTTPS URL (including the port number) of your Codec Server endpoint. You may diff --git a/docs/cloud/get-started/service-accounts.mdx b/docs/cloud/get-started/service-accounts.mdx index 11edf92ff3..5154078383 100644 --- a/docs/cloud/get-started/service-accounts.mdx +++ b/docs/cloud/get-started/service-accounts.mdx @@ -159,7 +159,15 @@ The Service Account is deleted when it is no longer visible in the output of the ### Update a Service Account {/* #update */} -Update a Service Account's description using the Temporal Cloud UI or tcld. +Update a Service Account using the Temporal Cloud UI or tcld. + +:::note Account roles and Namespace Permissions + +Service Accounts with the Account Owner or Global Admin account-level role automatically have Namespace Admin access to +all Namespaces. Do not add explicit Namespace Permissions while using either role. To move a Service Account from Global +Admin to a lower-privilege account role, update the Account Level Role and desired Namespace Permissions together. + +::: diff --git a/docs/cloud/high-availability/failovers/index.mdx b/docs/cloud/high-availability/failovers/index.mdx index 5a5d816ba6..70d6f98cf4 100644 --- a/docs/cloud/high-availability/failovers/index.mdx +++ b/docs/cloud/high-availability/failovers/index.mdx @@ -70,7 +70,7 @@ exhaustive list, and it may change over time. ::: -- Whether Temporal Cloud's services in the cell are reachable from the control plane. +- Whether Temporal Cloud's services in the cell are reachable from the Control Plane. - The average latency of inbound RPC calls (excluding long-polling APIs) to Temporal services in the cell. - The percentage of inbound RPC calls that returned errors related to server health. - The average latency of calls from Temporal Cloud's services in the cell to its persistence layer. diff --git a/docs/cloud/operation-api.mdx b/docs/cloud/operation-api.mdx index 498f2a1606..1369fbe710 100644 --- a/docs/cloud/operation-api.mdx +++ b/docs/cloud/operation-api.mdx @@ -2,7 +2,7 @@ id: operation-api title: Cloud Ops API sidebar_label: Cloud Ops API -description: The Temporal Cloud Operations API (Cloud Ops) allows programmatic management of Temporal Cloud control plane resources. +description: The Temporal Cloud Operations API (Cloud Ops) allows programmatic management of Temporal Cloud Control Plane resources. slug: /ops toc_max_heading_level: 4 keywords: @@ -18,7 +18,7 @@ The Temporal Cloud Operations API is in [Public Preview](/evaluate/development-p ::: -The Temporal Cloud Operations API, or the Cloud Ops API, is an open source, public [HTTP API](https://saas-api.tmprl.cloud/docs/httpapi.html#description/introduction) and [gRPC API](https://github.com/temporalio/cloud-api/tree/main) for programmatically managing Temporal Cloud control plane resources, including [Namespaces](/cloud/namespaces), [Users](/cloud/users), [Service Accounts](/cloud/service-accounts), [API keys](/cloud/api-keys), and others. The Temporal Cloud [Terraform Provider](/cloud/terraform-provider), [tcld CLI](/cloud/tcld), and Web UI all use the Cloud Ops API. +The Temporal Cloud Operations API, or the Cloud Ops API, is an open source, public [HTTP API](https://saas-api.tmprl.cloud/docs/httpapi.html#description/introduction) and [gRPC API](https://github.com/temporalio/cloud-api/tree/main) for programmatically managing Temporal Cloud Control Plane resources, including [Namespaces](/cloud/namespaces), [Users](/cloud/users), [Service Accounts](/cloud/service-accounts), [API keys](/cloud/api-keys), and others. The Temporal Cloud [Terraform Provider](/cloud/terraform-provider), [tcld CLI](/cloud/tcld), and Web UI all use the Cloud Ops API. ## Develop applications with the Cloud Ops API @@ -151,7 +151,7 @@ The Temporal Cloud Operations API implements rate limiting to ensure system stab **Total rate limit: 160 requests per second (RPS)** -This limit applies to all requests made to the Temporal Cloud control plane by any client (tcld, UI, Cloud Ops API) or identity type (user, service account) within your account. The total account throughput cannot exceed the limit regardless of the number of users or service accounts making requests. +This limit applies to all requests made to the Temporal Cloud Control Plane by any client (tcld, UI, Cloud Ops API) or identity type (user, service account) within your account. The total account throughput cannot exceed the limit regardless of the number of users or service accounts making requests. ### Per-identity rate limits @@ -169,7 +169,7 @@ This limits the number of concurrent asynchronous operations that can be in-flig ### Important considerations -- Rate limits are enforced across all Temporal Cloud control plane operations +- Rate limits are enforced across all Temporal Cloud Control Plane operations - Multiple clients used by the same identity (user or service account) share the same rate limit - Authentication method (SSO, API keys) does not affect rate limiting - These limits help ensure system stability and prevent any single account or identity from overwhelming the service diff --git a/docs/cloud/terraform-provider.mdx b/docs/cloud/terraform-provider.mdx index 7833d2aa17..3a0b841e88 100644 --- a/docs/cloud/terraform-provider.mdx +++ b/docs/cloud/terraform-provider.mdx @@ -465,7 +465,7 @@ For example, to change the allowed caller Namespaces on a Nexus Endpoint: ``` Upon completion, you will see a success message indicating your Nexus Endpoint has been updated. It may take several - seconds to update a Nexus Endpoint in the control plane which is visible from the Temporal UI or tcld CLI. + seconds to update a Nexus Endpoint in the Control Plane which is visible from the Temporal UI or tcld CLI. Propagation of Nexus Endpoint changes to the data plane may take longer, but usually complete in less than one minute. diff --git a/docs/cloud/worker-health.mdx b/docs/cloud/worker-health.mdx index 116a759d5b..22fe1873eb 100644 --- a/docs/cloud/worker-health.mdx +++ b/docs/cloud/worker-health.mdx @@ -384,7 +384,7 @@ This feature is currently in [Public Preview](/evaluate/development-production-f ::: Workers send a heartbeat to Temporal Server every 60 seconds by default. This heartbeat serves to provide liveness and configuration data from the Worker to the Server. -Specific data sent can be found in the [API](https://github.com/temporalio/api/blob/master/temporal/api/worker/v1/message.proto). By providing a consistent heartbeat from +Specific data sent can be found in the [API](https://github.com/temporalio/api/blob/main/temporal/api/worker/v1/message.proto). By providing a consistent heartbeat from Workers, the Server can obtain an accurate count of Workers, understand Worker performance, and respond to Worker heartbeats with commands. Some examples of how this is useful: - understanding the difference between a Worker that is down and a Worker that is processing tasks for a long time diff --git a/docs/develop/go/best-practices/data-handling/external-storage.mdx b/docs/develop/go/best-practices/data-handling/external-storage.mdx index 778ca84d4e..7836ebe659 100644 --- a/docs/develop/go/best-practices/data-handling/external-storage.mdx +++ b/docs/develop/go/best-practices/data-handling/external-storage.mdx @@ -93,8 +93,7 @@ w := worker.New(c, "my-task-queue", worker.Options{}) ## Implement a custom storage driver If you need a storage backend other than what the built-in drivers allow, you can implement your own storage driver. -Store payloads durably so that they survive process crashes and remain available for debugging and auditing after the -Workflow completes. Refer to [lifecycle management](/external-storage#lifecycle) for retention requirements. +Refer to [Choose a storage system](/external-storage#choose-storage) for guidance on selecting a backing store and [Lifecycle management](/external-storage#lifecycle) for retention requirements. The following example shows a custom driver that uses local disk as the backing store. This example is for local development and testing only. In production, use a durable storage system that is accessible to all Workers: diff --git a/docs/develop/go/best-practices/multithreading.mdx b/docs/develop/go/best-practices/multithreading.mdx index 1992ff0d39..3bcacd7048 100644 --- a/docs/develop/go/best-practices/multithreading.mdx +++ b/docs/develop/go/best-practices/multithreading.mdx @@ -48,4 +48,4 @@ For an example using Signals, refer to the [Go Await Signal Sample](https://gith ## Static analysis with the workflowcheck tool -The Temporal Go SDK also provides a command line tool called [`workflowcheck`](https://github.com/temporalio/sdk-go/blob/master/contrib/tools/workflowcheck/README.md) to statically analyze Workflow Definitions. This can help eliminate potential instances of non-determinism. +The Temporal Go SDK also provides a command line tool called [`workflowcheck`](https://github.com/temporalio/sdk-go/blob/main/contrib/tools/workflowcheck/README.md) to statically analyze Workflow Definitions. This can help eliminate potential instances of non-determinism. diff --git a/docs/develop/go/client/temporal-client.mdx b/docs/develop/go/client/temporal-client.mdx index 14289598e4..f481c06804 100644 --- a/docs/develop/go/client/temporal-client.mdx +++ b/docs/develop/go/client/temporal-client.mdx @@ -881,7 +881,7 @@ if err != nil { } ``` -[Sample](https://github.com/temporalio/samples-go/tree/master/cron) +[Sample](https://github.com/temporalio/samples-go/tree/main/cron) #### Memo diff --git a/docs/develop/go/workers/sessions.mdx b/docs/develop/go/workers/sessions.mdx index 6363e5746c..407a4bb638 100644 --- a/docs/develop/go/workers/sessions.mdx +++ b/docs/develop/go/workers/sessions.mdx @@ -190,7 +190,7 @@ token := workflow.GetSessionInfo(sessionCtx).GetRecreateToken() **Is there a complete example?** -Yes, the [file processing example](https://github.com/temporalio/samples-go/tree/master/fileprocessing) in the [temporalio/samples-go](https://github.com/temporalio/samples-go) repo has been updated to use the session framework. +Yes, the [file processing example](https://github.com/temporalio/samples-go/tree/main/fileprocessing) in the [temporalio/samples-go](https://github.com/temporalio/samples-go) repo has been updated to use the session framework. **What happens to my Activity if the Worker dies?** diff --git a/docs/develop/java/index.mdx b/docs/develop/java/index.mdx index 46f73981be..f71a645be4 100644 --- a/docs/develop/java/index.mdx +++ b/docs/develop/java/index.mdx @@ -66,6 +66,7 @@ From there, you can dive deeper into any of the Temporal primitives to start bui - [Quickstart](/develop/java/nexus/quickstart) - [Feature guide](/develop/java/nexus/feature-guide) +- [Standalone Operations](/develop/java/nexus/standalone-operations) ## [Platform](/develop/java/platform) diff --git a/docs/develop/java/nexus/index.mdx b/docs/develop/java/nexus/index.mdx index 2246063f16..4b68df76fc 100644 --- a/docs/develop/java/nexus/index.mdx +++ b/docs/develop/java/nexus/index.mdx @@ -25,4 +25,5 @@ Temporal Java SDK support for Nexus is [Generally Available](/evaluate/developme - [Quickstart](/develop/java/nexus/quickstart) - [Feature guide](/develop/java/nexus/feature-guide) +- [Standalone Operations](/develop/java/nexus/standalone-operations) - [Nexus sync tutorial](https://learn.temporal.io/tutorials/nexus/nexus-sync-tutorial/) diff --git a/docs/develop/java/nexus/standalone-operations.mdx b/docs/develop/java/nexus/standalone-operations.mdx new file mode 100644 index 0000000000..def425e49f --- /dev/null +++ b/docs/develop/java/nexus/standalone-operations.mdx @@ -0,0 +1,168 @@ +--- +id: standalone-operations +title: Standalone Nexus Operations - Java SDK +sidebar_label: Standalone Operations +toc_max_heading_level: 4 +keywords: + - standalone nexus operation + - nexus operation execution + - execute nexus operation + - nexus operation handle + - list nexus operations + - count nexus operations + - java sdk +tags: + - Nexus + - Temporal Client + - Java SDK + - Temporal SDKs +description: Execute Nexus Operations independently without a Workflow using the Temporal Java SDK. +--- + +:::tip SUPPORT, STABILITY, and DEPENDENCY INFO + +Temporal Java SDK support for Standalone Nexus Operations is at +[Pre-release](/evaluate/development-production-features/release-stages#pre-release). + +All APIs are experimental and may be subject to backwards-incompatible changes. + +::: + +[Standalone Nexus Operations](/standalone-nexus-operation) let you run Nexus Operation Executions independently, without +being orchestrated by a Workflow. Instead of calling a Nexus Operation from within a Workflow Definition using +`Workflow.newNexusServiceStub()`, you execute a Standalone Nexus Operation directly from a Nexus service client created +from a `NexusClient` using `NexusClient.newNexusServiceClient()`. + +Standalone Nexus Operations use the same Nexus Service contract, Operation handlers, and Worker setup as +Workflow-driven Operations — only the execution path differs. See the [Nexus feature guide](/develop/java/nexus/feature-guide) for details on +[defining a Service contract](/develop/java/nexus/feature-guide#define-nexus-service-contract), +[developing Operation handlers](/develop/java/nexus/feature-guide#develop-nexus-service-operation-handlers), and +[registering a Service in a Worker](/develop/java/nexus/feature-guide#register-a-nexus-service-in-a-worker). + +This page focuses on the client-side APIs that are unique to Standalone Nexus Operations: + +- [Execute a Standalone Nexus Operation](#execute-operation) +- [Start a Standalone Nexus Operation and Wait for the Result](#get-operation-result) +- [List Standalone Nexus Operations](#list-operations) +- [Count Standalone Nexus Operations](#count-operations) + +:::note +This documentation uses source code from the +[Java Nexus Standalone sample](https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/nexusstandalone). + +::: + +## Execute a Standalone Nexus Operation {/* #execute-operation */} + +To execute a Standalone Nexus Operation, first create a +[`NexusClient`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/client/NexusClient.html), then +derive a typed +[`NexusServiceClient`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/client/NexusServiceClient.html) +from it with `newNexusServiceClient()`, bound to a specific Nexus Endpoint and Service. The endpoint must be +pre-created on the server. Then call `start()` or `execute()` from application code (for example, a starter program), +not from inside a Workflow Definition. + +`execute()` waits for the Operation to complete and returns the result. +Both methods take a [`StartNexusOperationOptions`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/client/StartNexusOperationOptions.html) +whose `id` is required — the SDK never generates one for you. `scheduleToCloseTimeout` is optional and defaults to the +maximum allowed by the Temporal server. + +```java +NexusClient nexusClient = NexusClient.newInstance(stubs, options); +NexusServiceClient greetingClient = + nexusClient.newNexusServiceClient(GreetingNexusService.class, ENDPOINT_NAME); + +// Block until the operation completes and return its result. +GreetingOutput greeting = + greetingClient.execute( + GreetingNexusService::greet, + new GreetingInput("World"), + StartNexusOperationOptions.newBuilder() + .setId("greet-" + UUID.randomUUID()) + .setScheduleToCloseTimeout(Duration.ofSeconds(10)) + .build()); +``` + +`executeAsync()` is the same but returns a `CompletableFuture` instead of blocking. + +```java +CompletableFuture future = + greetingClient.executeAsync( + GreetingNexusService::greet, new GreetingInput("World"), options); +GreetingOutput greeting = future.get(); +``` + +See the full +[starter sample](https://github.com/temporalio/samples-java/blob/main/core/src/main/java/io/temporal/samples/nexusstandalone/StandaloneClientStarter.java) +for a complete example that executes both synchronous and asynchronous Operations, gets their results, and lists and +counts Operations. + +## Start a Standalone Nexus Operation and Wait for the Result {/* #get-operation-result */} + +`start()` returns a +[`NexusOperationHandle`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/client/NexusOperationHandle.html). +Use `NexusOperationHandle.getResult()` to wait until the Operation completes and retrieve its result. This works for +both synchronous and asynchronous Operations. + +```java +// Start an operation and get a NexusOperationHandle. +NexusOperationHandle handle = + greetingClient.start( + GreetingNexusService::startGreeting, new GreetingInput("World"), options); + +// Block until the operation completes and retrieve its result. +GreetingOutput greeting = handle.getResult(); +``` + +If the Operation completed successfully, the result is returned. If the Operation failed, the failure is thrown as a +`NexusOperationException`. Use `getResultAsync()` for a non-blocking `CompletableFuture`, or +`getResult(long timeout, TimeUnit unit)` to bound the wait. + +## List Standalone Nexus Operations {/* #list-operations */} + +Use [`NexusClient.listNexusOperationExecutions()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/client/NexusClient.html) +to list Standalone Nexus Operation Executions that match a [List Filter](/list-filter) query. The result is a `Stream` +of operation metadata entries. + +Note that `listNexusOperationExecutions()` is called on a `NexusClient`, not on the typed `NexusServiceClient`. + +```java +String query = "Endpoint = \"" + ENDPOINT_NAME + "\""; +nexusClient + .listNexusOperationExecutions(query) + .forEach( + op -> + System.out.printf( + "OperationId: %s, Operation: %s, Status: %s%n", + op.getOperationId(), op.getOperation(), op.getStatus())); +``` + +The `query` parameter accepts [List Filter](/list-filter) syntax. For example, +`"Endpoint = 'my-endpoint' AND ExecutionStatus = 'Running'"`. + +## Count Standalone Nexus Operations {/* #count-operations */} + +Use [`NexusClient.countNexusOperationExecutions()`](https://www.javadoc.io/doc/io.temporal/temporal-sdk/latest/io/temporal/client/NexusClient.html) +to count Standalone Nexus Operation Executions that match a [List Filter](/list-filter) query. + +Note that `countNexusOperationExecutions()` is called on a `NexusClient`, not on the typed `NexusServiceClient`. + +```java +String query = "Endpoint = \"" + ENDPOINT_NAME + "\""; +NexusOperationExecutionCount count = nexusClient.countNexusOperationExecutions(query); +System.out.println("Total Nexus operations: " + count.getCount()); +``` + +Passing a `GROUP BY` query (for example, `"GROUP BY ExecutionStatus"`) returns a count per group, available through +`NexusOperationExecutionCount.getGroups()`. + +## Run Standalone Nexus Operations with Temporal Cloud {/* #run-standalone-nexus-operations-temporal-cloud */} + +The code samples referenced on this page build their client from a `ClientConfigProfile` loaded from a TOML profile, so +the same code works against Temporal Cloud — just point the profile at your Cloud Namespace (or override the connection +via `TEMPORAL_*` environment variables). No code changes are needed. + +For full details on connecting to Temporal Cloud, including Namespace creation, Nexus Endpoint setup, certificate +generation, and authentication options, see +[Make Nexus calls across Namespaces in Temporal Cloud](/develop/java/nexus/feature-guide#nexus-calls-across-namespaces-temporal-cloud) +and [Connect to Temporal Cloud](/develop/java/client/temporal-client#connect-to-temporal-cloud). diff --git a/docs/develop/python/best-practices/data-handling/external-storage.mdx b/docs/develop/python/best-practices/data-handling/external-storage.mdx index a2c2bc018c..4dbe19f829 100644 --- a/docs/develop/python/best-practices/data-handling/external-storage.mdx +++ b/docs/develop/python/best-practices/data-handling/external-storage.mdx @@ -92,11 +92,11 @@ failures. For a complete working example that includes a Worker, Codec Server, a ## Implement a custom storage driver If you need a storage backend other than what the built-in drivers allow, you can implement your own storage driver. -Store payloads durably so that they survive process crashes and remain available for debugging and auditing after the -Workflow completes. Refer to [lifecycle management](/external-storage#lifecycle) for retention requirements. +Refer to [Choose a storage system](/external-storage#choose-storage) for guidance on selecting a backing store and [Lifecycle management](/external-storage#lifecycle) for retention requirements. The following example shows a custom driver that uses local disk as the backing store. This example is for local -development and testing only. In production, use a durable storage system that is accessible to all Workers: +development and testing only. In production, use a durable storage system that is accessible to all Workers. +For example, see the [Redis storage driver sample](https://github.com/temporalio/samples-python/tree/main/external_storage_redis). [features/snippets/external_storage/custom_driver/custom_storage_driver.py](https://github.com/temporalio/features/blob/main/features/snippets/external_storage/custom_driver/custom_storage_driver.py) diff --git a/docs/develop/python/index.mdx b/docs/develop/python/index.mdx index 403be4645b..32811593f9 100644 --- a/docs/develop/python/index.mdx +++ b/docs/develop/python/index.mdx @@ -86,6 +86,7 @@ From there, you can dive deeper into any of the Temporal primitives to start bui - [LangSmith integration](/develop/python/integrations/langsmith) - [OpenAI Agents SDK integration](https://github.com/temporalio/sdk-python/blob/main/temporalio/contrib/openai_agents/README.md) - [Pydantic AI integration](https://ai.pydantic.dev/durable_execution/temporal/) +- [Strands Agents integration](/develop/python/integrations/strands-agents) - [Tenuo integration](https://tenuo.ai/temporal) ## Temporal Python Technical Resources diff --git a/docs/develop/python/integrations/index.mdx b/docs/develop/python/integrations/index.mdx index e0f3fb8d8a..8bdad96bd4 100644 --- a/docs/develop/python/integrations/index.mdx +++ b/docs/develop/python/integrations/index.mdx @@ -28,6 +28,7 @@ The following integrations are available between the Temporal Python SDK and thi | LangSmith | Observability | [smith.langchain.com](https://docs.smith.langchain.com/) | [Guide](./langsmith.mdx) | | OpenAI Agents SDK | Agent framework | [openai.github.io](https://openai.github.io/openai-agents-python/) | [Guide](https://github.com/temporalio/sdk-python/blob/main/temporalio/contrib/openai_agents/README.md) | | Pydantic AI | Agent framework | [ai.pydantic.dev](https://ai.pydantic.dev/) | [Guide](https://ai.pydantic.dev/durable_execution/temporal/) | +| Strands Agents | Agent framework | [strandsagents.com](https://strandsagents.com/) | [Guide](./strands-agents.mdx) | | Tenuo | Governance | [tenuo.ai](https://tenuo.ai/docs) | [Guide](https://tenuo.ai/temporal) | These integrations are built on the Temporal Python SDK's [Plugin system](/develop/plugins-guide), which you can also diff --git a/docs/develop/python/integrations/strands-agents.mdx b/docs/develop/python/integrations/strands-agents.mdx new file mode 100644 index 0000000000..f4734daced --- /dev/null +++ b/docs/develop/python/integrations/strands-agents.mdx @@ -0,0 +1,945 @@ +--- +id: strands-agents +title: Strands Agents integration +sidebar_label: Strands Agents +toc_max_heading_level: 3 +keywords: + - ai + - agents + - strands + - strands agents + - durable execution + - ai workflows +tags: + - Strands Agents + - Python SDK + - Temporal SDKs +description: Run Strands Agents AI workflows with durable execution using the Temporal Python SDK and Strands plugin. +--- + +Temporal's integration with [Strands Agents](https://strandsagents.com/) is an [SDK Plugin](/develop/plugins-guide) that +gives your Strands agents [Durable Execution](/temporal#durable-execution) via the Temporal platform. The plugin routes +model invocations, tool calls, MCP tool calls, and hooks through Temporal Activities, so every step your agent takes is +recorded in Workflow history and can survive crashes, restarts, and infrastructure failures. + +:::info + +The Temporal Python SDK integration with Strands Agents is currently at an experimental release stage. The API may +change in future versions. + +::: + +Code snippets in this guide are taken from the +[Strands Agents plugin samples](https://github.com/temporalio/samples-python/tree/main/strands_plugin). Refer to the +samples for the complete code. + +## Get started + +Install the plugin, then run a minimal Strands agent inside a Temporal Workflow. + +### Prerequisites + +- This guide assumes you are already familiar with Strands Agents. If you are not, refer to the + [Strands Agents documentation](https://strandsagents.com/) for more details. +- If you are new to Temporal, read [Understanding Temporal](/evaluate/understanding-temporal) or take the + [Temporal 101](https://learn.temporal.io/courses/temporal_101/) course. +- Set up your local development environment by following the + [Set up your local development environment](/develop/python/set-up-your-local-python) guide. Leave the Temporal + development server running if you want to test your code locally. + +### Install the plugin + +Install the Temporal Python SDK with Strands Agents support (requires `temporalio` 1.28.0 or later): + +```bash +uv add "temporalio[strands-agents]" +``` + +or with pip: + +```bash +pip install "temporalio[strands-agents]" +``` + +### Run a Strands agent with Durable Execution + +The following example runs a Strands agent inside a Temporal Workflow. Model calls execute as Temporal Activities, which +means they get automatic retries, timeouts, and durable execution. If the Worker process crashes mid-conversation, +Temporal replays the Workflow and resumes from the last completed Activity. + +**1. Define the Workflow** + +Create a Workflow that holds a `TemporalAgent` and invokes it with a prompt. The `start_to_close_timeout` sets the +maximum time each model call Activity can run: + + + +[strands_plugin/hello_world/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hello_world/workflow.py) + +```py +from datetime import timedelta + +from temporalio import workflow +from temporalio.contrib.strands import TemporalAgent + + +@workflow.defn +class HelloWorldWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent(start_to_close_timeout=timedelta(seconds=60)) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) +``` + + + +:::caution + +Inside a Workflow, always call `agent.invoke_async(message)`, not `agent(message)`. The synchronous form spawns a worker +thread, which the Workflow sandbox blocks. + +::: + +**2. Start a Worker** + +Create a Worker that registers the Workflow and the `StrandsPlugin`. The plugin automatically registers the Activities +that handle model calls: + + + +[strands_plugin/hello_world/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hello_world/run_worker.py) + +```py +import asyncio +import os + +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin +from temporalio.worker import Worker + +from strands_plugin.hello_world.workflow import HelloWorldWorkflow + + +async def main() -> None: + plugin = StrandsPlugin() + client = await Client.connect( + os.environ.get("TEMPORAL_ADDRESS", "localhost:7233"), + plugins=[plugin], + ) + + worker = Worker( + client, + task_queue="strands-hello-world", + workflows=[HelloWorldWorkflow], + ) + print("Worker started. Ctrl+C to exit.") + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + +**3. Run the Workflow** + +Start the Workflow from a separate client script. This example sends the prompt "Write a haiku about durable execution" +and prints the agent's response: + + + +[strands_plugin/hello_world/run_workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hello_world/run_workflow.py) + +```py +import asyncio +import os + +from temporalio.client import Client + +from strands_plugin.hello_world.workflow import HelloWorldWorkflow + + +async def main() -> None: + client = await Client.connect(os.environ.get("TEMPORAL_ADDRESS", "localhost:7233")) + + result = await client.execute_workflow( + HelloWorldWorkflow.run, + "Write a haiku about durable execution.", + id="strands-hello-world", + task_queue="strands-hello-world", + ) + + print(f"Result: {result}") + + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + +## Build the agent + +Customize which model provider your agent uses, add tools that run as Activities, subscribe to lifecycle events with +hooks, and connect to MCP servers. + +### Choose and configure models + +By default, `StrandsPlugin` uses Strands' own default model (`BedrockModel`). To use a different model, pass a `models` +mapping to `StrandsPlugin` on the Worker. When you provide a custom `models` mapping, each `TemporalAgent` must specify +which model to use by name. + +Each entry in the mapping pairs a name with a factory function that creates a model provider (such as `AnthropicModel` +or `BedrockModel`). The provider is created on first use and reused for the Worker's lifetime: + +```python +from strands.models.anthropic import AnthropicModel +from strands.models.bedrock import BedrockModel + +# Workflow +@workflow.defn +class MultiModelWorkflow: + def __init__(self) -> None: + self.agent_a = TemporalAgent( + model="claude", + start_to_close_timeout=timedelta(seconds=60), + ) + self.agent_b = TemporalAgent( + model="bedrock", + start_to_close_timeout=timedelta(seconds=60), + ) + +# Worker +Worker(..., plugins=[StrandsPlugin(models={ + "claude": lambda: AnthropicModel(client_args={"api_key": "..."}), + "bedrock": lambda: BedrockModel(), +})]) +``` + +Each `TemporalAgent` carries its own Activity options (timeouts, retry policy, task queue, streaming topic) and +dispatches to a shared model Activity, which resolves the model name against the registered factories at runtime. A +model name not present in the `models` mapping raises `ValueError` inside the Activity. + +### Run non-deterministic tools as Activities + +Strands tools that perform I/O, access external services, or produce non-deterministic results need to run as Temporal +Activities rather than inline in the Workflow. Wrap each tool in an `@activity.defn` function, register the Activities +on the Worker, and pass them to the agent using `activity_as_tool`. + +Define an Activity for the tool: + + + +[strands_plugin/tools/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/tools/workflow.py) + +```py +@activity.defn +async def fetch_weather(city: str) -> dict: + """Stub weather lookup — replace with a real HTTP call in production.""" + return { + "city": city, + "temperature_f": 72, + "conditions": "sunny", + } +``` + + + +Pass the Activity to the agent in the Workflow using `activity_as_tool`: + + + +[strands_plugin/tools/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/tools/workflow.py) + +```py +@workflow.defn +class ToolsWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + tools=[ + letter_counter, + activity_as_tool( + fetch_weather, + start_to_close_timeout=timedelta(seconds=30), + ), + activity_as_tool( + environment_activity, + start_to_close_timeout=timedelta(seconds=30), + ), + ], + ) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) +``` + + + +Register the Activity functions on the Worker: + + + +[strands_plugin/tools/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/tools/run_worker.py) + +```py +import asyncio +import os + +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin +from temporalio.worker import Worker + +from strands_plugin.tools.workflow import ( + ToolsWorkflow, + environment_activity, + fetch_weather, +) + + +async def main() -> None: + plugin = StrandsPlugin() + client = await Client.connect( + os.environ.get("TEMPORAL_ADDRESS", "localhost:7233"), + plugins=[plugin], + ) + + worker = Worker( + client, + task_queue="strands-tools", + workflows=[ToolsWorkflow], + activities=[fetch_weather, environment_activity], + ) + print("Worker started. Ctrl+C to exit.") + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + +If you are using built-in `strands_tools`, wrap them in a thin async function decorated with `@activity.defn` so they +run as Temporal Activities. + +### React to agent lifecycle events + +Strands' [hook system](https://strandsagents.com/docs/user-guide/concepts/agents/hooks/) lets you subscribe callbacks to events in the agent lifecycle, such +as invocation start/end, model call before/after, tool call before/after, and message added. Use hooks to add logging, +metrics, or custom logic at each stage. + +Pass `hooks=[MyHookProvider()]` to `TemporalAgent`. Hook callbacks fire in Workflow context, so deterministic callbacks +work without any extra setup. + +For callbacks that need I/O (audit logging, metrics, alerting), use `activity_as_hook` to dispatch the work as a +Temporal Activity. The following example shows both patterns in one `HookProvider`. The `_record` callback runs in +Workflow context (deterministic), while `persist_tool_call` runs as an Activity (I/O-safe): + + + +[strands_plugin/hooks/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hooks/workflow.py) + +```py +@activity.defn +async def persist_tool_call(tool_name: str) -> None: + # In production, write to a database / S3 / your audit pipeline. + activity.logger.info(f"audit: tool {tool_name} completed") +``` + + + + + +[strands_plugin/hooks/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/hooks/workflow.py) + +```py +class AuditHook(HookProvider): + def __init__(self) -> None: + self.fired: list[str] = [] + + def register_hooks(self, registry: HookRegistry, **kwargs: object) -> None: + registry.add_callback(AfterToolCallEvent, self._record) + registry.add_callback( + AfterToolCallEvent, + activity_as_hook( + persist_tool_call, + activity_input=lambda event: event.tool_use["name"], + start_to_close_timeout=timedelta(seconds=15), + ), + ) + + def _record(self, event: AfterToolCallEvent) -> None: + self.fired.append(event.tool_use["name"]) +``` + + + +:::caution + +Hook callbacks run in Workflow context, so they must be +[deterministic](/develop/python/workflows/basics#workflow-logic-requirements). Do not use `time.time()`, `uuid.uuid4()`, +or I/O inside hook callbacks. Use `activity_as_hook` for anything that requires I/O. + +::: + +The `activity_input` parameter extracts serializable values from the event to pass as the Activity's input. Use a +dataclass or Pydantic model for multiple values. This is needed because hook events hold references to `Agent`, +`AgentTool` instances, and other objects that cannot cross the Activity boundary. + +### Connect to MCP servers + +If your agent needs access to tools provided by an [MCP](https://modelcontextprotocol.io/) server, configure the MCP +clients on the Worker and reference them by name in the Workflow. + +`StrandsPlugin(mcp_clients=...)` takes a mapping of `name` to `MCPClient` factory, mirroring the `models` pattern. The +plugin registers a per-server Activity and connects at Worker startup to enumerate available tools. In the Workflow, +`TemporalMCPClient(server="name")` is a handle that references the server by name and carries per-call Activity options. + +Define the Workflow with a `TemporalMCPClient`: + + + +[strands_plugin/mcp/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/mcp/workflow.py) + +```py +from datetime import timedelta + +from temporalio import workflow +from temporalio.contrib.strands import TemporalAgent, TemporalMCPClient + + +@workflow.defn +class MCPWorkflow: + def __init__(self) -> None: + echo = TemporalMCPClient( + server="echo", + start_to_close_timeout=timedelta(seconds=30), + ) + self.agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + tools=[echo], + ) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) +``` + + + +Register the MCP client factory on the Worker: + + + +[strands_plugin/mcp/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/mcp/run_worker.py) + +```py +# ... +from mcp import StdioServerParameters, stdio_client +from strands.tools.mcp.mcp_client import MCPClient +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin +from temporalio.worker import Worker +# ... +def _make_echo_client() -> MCPClient: + return MCPClient( + lambda: stdio_client( + StdioServerParameters( + command=sys.executable, + args=[str(ECHO_SERVER)], + ) + ) + ) +# ... +async def main() -> None: + plugin = StrandsPlugin(mcp_clients={"echo": _make_echo_client}) + client = await Client.connect( + os.environ.get("TEMPORAL_ADDRESS", "localhost:7233"), + plugins=[plugin], + ) + + worker = Worker( + client, + task_queue="strands-mcp", + workflows=[MCPWorkflow], + ) + print("Worker started. Ctrl+C to exit.") + await worker.run() +``` + + + +Each factory returns a fully configured `MCPClient`, so you can pass options like `tool_filters`, `prefix`, +`elicitation_callback`, or `tasks_config` to it. + +:::info + +The plugin connects to each MCP server once at Worker startup to enumerate tools. The schema is frozen for the Worker's +lifetime. Restart Workers to pick up MCP server changes. If a server is unavailable at startup, the Worker fails to +start. + +::: + +## Interact with the agent + +Control the shape of agent responses, stream output in real time, and pause the agent for human approval. + +### Add human approval gates + +Some agent actions, such as deleting resources or sending messages, may require human approval before proceeding. +Strands offers two ways to interrupt an agent and wait for a response. Both work with the plugin. + +In each case, `agent.invoke_async()` returns `AgentResult(stop_reason="interrupt", interrupts=[...])` instead of +raising. Pair this with a Signal handler that supplies responses, then resume by calling +`agent.invoke_async(responses)`. + +#### Interrupt from a hook + +A hook on an interruptible event such as `BeforeToolCallEvent` can pause the agent by calling +`event.interrupt(name, reason=...)`. The hook runs in Workflow context, so it must be deterministic. + +Define the approval hook: + + + +[strands_plugin/human_in_the_loop/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/human_in_the_loop/workflow.py) + +```py +class ApprovalHook(HookProvider): + def register_hooks(self, registry: HookRegistry, **kwargs: object) -> None: + registry.add_callback(BeforeToolCallEvent, self._gate) + + def _gate(self, event: BeforeToolCallEvent) -> None: + if event.tool_use["name"] != "delete_file": + return + approval = event.interrupt( + "approval", + reason=f"approve delete of {event.tool_use['input']['path']}?", + ) + if approval != "approve": + event.cancel_tool = "denied" +``` + + + +The Workflow waits for a Signal carrying the approval response, then resumes the agent: + + + +[strands_plugin/human_in_the_loop/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/human_in_the_loop/workflow.py) + +```py +@workflow.defn +class HumanInTheLoopWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + tools=[delete_file], + hooks=[ApprovalHook()], + ) + self._approval: Optional[str] = None + self._pending_reason: Optional[str] = None + + @workflow.signal + def approve(self, response: str) -> None: + self._approval = response + + @workflow.query + def pending_approval(self) -> Optional[str]: + return self._pending_reason + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + while result.stop_reason == "interrupt": + interrupts = list(result.interrupts or []) + self._pending_reason = interrupts[0].reason if interrupts else None + await workflow.wait_condition(lambda: self._approval is not None) + response = self._approval + self._approval = None + self._pending_reason = None + responses: list[InterruptResponseContent] = [ + {"interruptResponse": {"interruptId": i.id, "response": response}} + for i in interrupts + ] + result = await self.agent.invoke_async(responses) + return str(result) +``` + + + +#### Interrupt from a tool + +A `@strands.tool` function can raise `InterruptException(Interrupt(...))` directly. The agent stops with the interrupt, +and the Workflow handles the resume the same way as for hooks: + +```python +from strands import tool +from strands.interrupt import Interrupt, InterruptException + + +@tool +def delete_thing(name: str) -> str: + raise InterruptException( + Interrupt(id=f"delete:{name}", name="approval", reason=f"delete {name}?") + ) +``` + +The same approach works from an `activity_as_tool`-wrapped Activity. The plugin's failure converter preserves the +`Interrupt` payload across the Activity boundary, so `AgentResult.interrupts` is populated the same way. + +Define the Activity that raises the interrupt: + + + +[strands_plugin/activity_interrupt/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/activity_interrupt/workflow.py) + +```py +@activity.defn +async def delete_thing(name: str) -> str: + if name not in _APPROVED: + _APPROVED.add(name) + raise InterruptException( + Interrupt( + id=f"delete:{name}", + name="approval", + reason=f"approve delete of protected resource '{name}'?", + ) + ) + return f"deleted {name}" +``` + + + +:::caution + +Activity-tool interrupts rely on the plugin's failure converter, which is installed via the client's data converter. +Attach `StrandsPlugin` to the **client** (not just the Worker) for Activity-tool interrupts to work. + +::: + +Workers built from that client pick up the plugin automatically: + + + +[strands_plugin/activity_interrupt/run_worker.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/activity_interrupt/run_worker.py) + +```py +import asyncio +import os + +from temporalio.client import Client +from temporalio.contrib.strands import StrandsPlugin +from temporalio.worker import Worker + +from strands_plugin.activity_interrupt.workflow import ( + ActivityInterruptWorkflow, + delete_thing, +) + + +async def main() -> None: + plugin = StrandsPlugin() + # The plugin MUST be on the client so its failure converter is installed. + client = await Client.connect( + os.environ.get("TEMPORAL_ADDRESS", "localhost:7233"), + plugins=[plugin], + ) + + worker = Worker( + client, + task_queue="strands-activity-interrupt", + workflows=[ActivityInterruptWorkflow], + activities=[delete_thing], + ) + print("Worker started. Ctrl+C to exit.") + await worker.run() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + +### Return structured data from an agent + +To have the agent return a typed object instead of free-form text, pass a `structured_output_model` to `TemporalAgent`. +The plugin defaults to the [`pydantic_data_converter`](/develop/python/data-handling/data-conversion), so Pydantic types +serialize cleanly across the Activity and Workflow boundary: + + + +[strands_plugin/structured_output/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/structured_output/workflow.py) + +```py +from datetime import timedelta + +from pydantic import BaseModel, Field +from temporalio import workflow +from temporalio.contrib.strands import TemporalAgent + + +class PersonInfo(BaseModel): + name: str = Field(description="Name of the person") + age: int = Field(description="Age of the person") + occupation: str = Field(description="Occupation of the person") + + +@workflow.defn +class StructuredOutputWorkflow: + def __init__(self) -> None: + self.agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + structured_output_model=PersonInfo, + ) + + @workflow.run + async def run(self, prompt: str) -> PersonInfo: + result = await self.agent.invoke_async(prompt) + assert isinstance(result.structured_output, PersonInfo) + return result.structured_output +``` + + + +### Stream agent output to clients + +For long-running agent calls, you may want to forward model output chunks to an external consumer as they arrive rather +than waiting for the full response. + +Pass `streaming_topic="..."` to `TemporalAgent` and host a `WorkflowStream` on the Workflow. Each `StreamEvent` is +published from inside the model Activity. Subscribers read events through `WorkflowStreamClient`. Chunks are batched on +`streaming_batch_interval` (default 100 ms). + +Define the Workflow with a `WorkflowStream` and a streaming topic: + + + +[strands_plugin/streaming/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/streaming/workflow.py) + +```py +from datetime import timedelta + +from temporalio import workflow +from temporalio.contrib.strands import TemporalAgent +from temporalio.contrib.workflow_streams import WorkflowStream + + +@workflow.defn +class StreamingWorkflow: + def __init__(self) -> None: + self.stream = WorkflowStream() + self.agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + streaming_topic="events", + ) + + @workflow.run + async def run(self, prompt: str) -> str: + result = await self.agent.invoke_async(prompt) + return str(result) +``` + + + +Subscribe to the stream from a client: + + + +[strands_plugin/streaming/run_workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/streaming/run_workflow.py) + +```py +import asyncio +import os +from datetime import timedelta + +from strands.types.streaming import StreamEvent +from temporalio.client import Client +from temporalio.contrib.workflow_streams import WorkflowStreamClient + +from strands_plugin.streaming.workflow import StreamingWorkflow + + +async def main() -> None: + client = await Client.connect(os.environ.get("TEMPORAL_ADDRESS", "localhost:7233")) + workflow_id = "strands-streaming" + + handle = await client.start_workflow( + StreamingWorkflow.run, + "Count from 1 to 5, one number per sentence.", + id=workflow_id, + task_queue="strands-streaming", + ) + + async def consume() -> None: + stream = WorkflowStreamClient.create(client, workflow_id) + async for item in stream.subscribe( + ["events"], + from_offset=0, + result_type=StreamEvent, + poll_cooldown=timedelta(milliseconds=50), + ): + event: StreamEvent = item.data + if "contentBlockDelta" in event: + delta = event["contentBlockDelta"].get("delta", {}) + if "text" in delta: + print(delta["text"], end="", flush=True) + elif "messageStop" in event: + print() + return + + consume_task = asyncio.create_task(consume()) + result = await handle.result() + await asyncio.wait_for(consume_task, timeout=10.0) + print(f"Final result: {result}") + + +if __name__ == "__main__": + asyncio.run(main()) +``` + + + +## Run in production + +Configure retry policies, handle long-running chat sessions, and add distributed tracing. + +### Configure retries + +`TemporalAgent` disables Strands' built-in `ModelRetryStrategy` so that retries are handled exclusively by Temporal. +Configure retries with `retry_policy` on `TemporalAgent` for model calls, and on the Activity options accepted by +`activity_as_tool`, `activity_as_hook`, and `TemporalMCPClient` for their respective calls: + +```python +from temporalio.common import RetryPolicy + + +TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + retry_policy=RetryPolicy(maximum_attempts=3), +) +``` + +Passing `retry_strategy=...` to `TemporalAgent(...)` raises `ValueError`. Remove the argument (or pass +`retry_strategy=None`) and use `retry_policy` instead. + +### Handle long-running chat sessions + +A chat-style Workflow accumulates message history with every turn. Over a long session, the Workflow's event history can +grow large enough to hit Temporal's per-Workflow history limit. To avoid this, use +[Continue-as-New](/develop/python/workflows/continue-as-new) to start a fresh Workflow execution while carrying the +agent's message history forward as input. + +In this example, each user turn arrives as a Workflow [Update](/develop/python/workflows/message-passing#updates), so +the caller gets the agent's reply back from the same call. The `run` method creates the agent, then waits until either +the chat ends or Temporal suggests continue-as-new. When it does, the Workflow drains any in-flight updates and starts a +fresh execution with the agent's accumulated messages: + + + +[strands_plugin/continue_as_new/workflow.py](https://github.com/temporalio/samples-python/blob/main/strands_plugin/continue_as_new/workflow.py) + +```py +import asyncio +from dataclasses import dataclass, field +from datetime import timedelta + +from strands.types.content import Messages +from temporalio import workflow +from temporalio.contrib.strands import TemporalAgent + + +@dataclass +class ChatInput: + messages: Messages = field(default_factory=list) + + +@workflow.defn +class ChatWorkflow: + def __init__(self) -> None: + self._done = False + self._lock = asyncio.Lock() + self._agent: TemporalAgent | None = None + + @workflow.update + async def turn(self, prompt: str) -> str: + await workflow.wait_condition(lambda: self._agent is not None) + async with self._lock: + assert self._agent is not None + result = await self._agent.invoke_async(prompt) + return str(result).strip() + + @workflow.signal + def end_chat(self) -> None: + self._done = True + + @workflow.query + def messages(self) -> Messages: + return list(self._agent.messages) if self._agent else [] + + @workflow.run + async def run(self, input: ChatInput) -> None: + self._agent = TemporalAgent( + start_to_close_timeout=timedelta(seconds=60), + messages=list(input.messages), + ) + + await workflow.wait_condition( + lambda: self._done or workflow.info().is_continue_as_new_suggested() + ) + + await workflow.wait_condition(workflow.all_handlers_finished) + + if not self._done: + workflow.continue_as_new(ChatInput(messages=self._agent.messages)) +``` + + + +### Add tracing with OpenTelemetry + +To get distributed traces across model, tool, and MCP Activities, combine `StrandsPlugin` with the +[OpenTelemetry plugin](/develop/python/platform/observability#tracing). Register `OpenTelemetryPlugin` on the client and +`StrandsPlugin` on the Worker. Workers built from that client pick up the OpenTelemetry plugin automatically: + +```python +import opentelemetry.trace +from temporalio.client import Client +from temporalio.contrib.opentelemetry import OpenTelemetryPlugin, create_tracer_provider +from temporalio.contrib.strands import StrandsPlugin +from temporalio.worker import Worker + + +opentelemetry.trace.set_tracer_provider(create_tracer_provider()) + +client = await Client.connect("localhost:7233", plugins=[OpenTelemetryPlugin()]) + +Worker( + client, + task_queue="strands", + workflows=[MyWorkflow], + plugins=[StrandsPlugin()], +) +``` + +Set the tracer provider before connecting the client. + +### Snapshots are not supported + +`TemporalAgent.take_snapshot()` and `TemporalAgent.load_snapshot()` raise `NotImplementedError`. Temporal's event +history already persists Workflow state durably at a finer granularity than Strands snapshots, so snapshots are +redundant inside a Workflow. + +### Samples + +The [Strands Agents plugin samples](https://github.com/temporalio/samples-python/tree/main/strands_plugin) demonstrate +all supported patterns end-to-end. diff --git a/docs/encyclopedia/data-conversion/external-storage.mdx b/docs/encyclopedia/data-conversion/external-storage.mdx index 19441bb54b..061c0f0331 100644 --- a/docs/encyclopedia/data-conversion/external-storage.mdx +++ b/docs/encyclopedia/data-conversion/external-storage.mdx @@ -100,6 +100,26 @@ payload from external storage before returning it to your Workflow or Client. Because External Storage runs after the Payload Codec, if you use an encryption codec, payloads are already encrypted before upload to your store. +## Choose a storage system {/* #choose-storage */} + +A production storage system should meet the following requirements: + +- Store payload data durably and retain it for the full Workflow lifetime plus the Namespace retention period. See + [Lifecycle management](#lifecycle) for details. +- Be reachable from every Client, Worker, and Codec Server that encodes or decodes payloads. +- Support your expected payload sizes. +- Return consistent data immediately after a write completes. +- Meet your latency and throughput requirements under realistic load. +- Provide appropriate controls for authentication, encryption, monitoring, and backup. + +Start with an object store such as Amazon S3, Google Cloud Storage, or Azure Blob Storage unless you have a specific +reason to use a different system, such as lower latency or existing infrastructure constraints. + +- **Amazon S3, Google Cloud Storage, Azure Blob Storage:** Default choice for durable payload storage. We provide a + first-party S3 storage driver for the [Go](/develop/go/data-handling/external-storage#store-and-retrieve-large-payloads-with-amazon-s3) and [Python](/develop/python/data-handling/external-storage#store-and-retrieve-large-payloads-with-amazon-s3) SDKs. +- **Google Cloud Bigtable:** Low-latency reads on Google Cloud, but payloads must fit within Bigtable's cell and row size limits. +- **Redis:** Suitable when configured for durability (such as with AOF persistence), not as an evicting cache. Refer to the [Python Redis storage driver sample](https://github.com/temporalio/samples-python/tree/main/external_storage_redis) for an example implementation. + ## Storage drivers A storage driver connects External Storage to a backing store. Each driver provides two operations: @@ -123,6 +143,9 @@ examples, see: - [Go SDK: Implement a custom storage driver](/develop/go/data-handling/external-storage#implement-a-custom-storage-driver) - [Python SDK: Implement a custom storage driver](/develop/python/data-handling/external-storage#implement-a-custom-storage-driver) +For example, see the +[Redis storage driver sample](https://github.com/temporalio/samples-python/tree/main/external_storage_redis). + ## Key configuration settings Configure External Storage on the Data Converter. The key settings are: diff --git a/docs/encyclopedia/data-conversion/key-management.mdx b/docs/encyclopedia/data-conversion/key-management.mdx index e1c0505495..0ebbf81b1c 100644 --- a/docs/encyclopedia/data-conversion/key-management.mdx +++ b/docs/encyclopedia/data-conversion/key-management.mdx @@ -45,7 +45,3 @@ It is recommended that operators estimate the encryption rate of a key and use t Key rotation should generally be transparent to the Temporal Data Converter implementation. Temporal's `Encode()` and `Decode()` steps only need to trigger as expected, and Temporal has no knowledge of how or when you are generating your encryption keys. You should design your Encode and Decode steps to accept all the necessary parameters for your key management, such as the key version, alongside your payloads. Like the Data Converters, keys should be mapped to a Namespace in Temporal. - -### Using Vault for Key Management - -[This repository](https://github.com/zboralski/codecserver) provides a robust and complete example of using Temporal with HashiCorp's [Vault](https://www.vaultproject.io/) secrets engine. diff --git a/docs/encyclopedia/nexus/nexus-registry.mdx b/docs/encyclopedia/nexus/nexus-registry.mdx index b5b6049df8..1dd0ca2929 100644 --- a/docs/encyclopedia/nexus/nexus-registry.mdx +++ b/docs/encyclopedia/nexus/nexus-registry.mdx @@ -126,6 +126,6 @@ There are two ways to automate endpoint provisioning and lifecycle management: T - [Terraform support](/cloud/terraform-provider#manage-temporal-cloud-nexus-endpoints-with-terraform) for Temporal Cloud. - [Cloud Ops API](/ops) for Temporal Cloud. -- [Operator API](https://github.com/temporalio/api/blob/master/temporal/api/operatorservice/v1/service.proto) for self-hosted deployments. +- [Operator API](https://github.com/temporalio/api/blob/main/temporal/api/operatorservice/v1/service.proto) for self-hosted deployments. ::: diff --git a/docs/encyclopedia/temporal-service/temporal-server.mdx b/docs/encyclopedia/temporal-service/temporal-server.mdx index cb21a01952..c21702791a 100644 --- a/docs/encyclopedia/temporal-service/temporal-server.mdx +++ b/docs/encyclopedia/temporal-service/temporal-server.mdx @@ -78,7 +78,7 @@ Temporal offers official support for, and is tested against, dependencies with t ## What is a Frontend Service? {/* #frontend-service */} -The Frontend Service is a stateless gateway service that exposes a strongly typed [Proto API](https://github.com/temporalio/api/blob/master/temporal/api/workflowservice/v1/service.proto). +The Frontend Service is a stateless gateway service that exposes a strongly typed [Proto API](https://github.com/temporalio/api/blob/main/temporal/api/workflowservice/v1/service.proto). The Frontend Service is responsible for rate limiting, authorizing, validating, and routing all inbound calls. -Commands are described in the [Command reference](/references/commands) and are defined in the [Temporal gRPC API](https://github.com/temporalio/api/blob/master/temporal/api/command/v1/message.proto). +Commands are described in the [Command reference](/references/commands) and are defined in the [Temporal gRPC API](https://github.com/temporalio/api/blob/main/temporal/api/command/v1/message.proto). ### Status {/* #workflow-execution-status */} diff --git a/docs/encyclopedia/workflow/workflow-execution/workflowid-runid.mdx b/docs/encyclopedia/workflow/workflow-execution/workflowid-runid.mdx index 7183454792..23bf90699e 100644 --- a/docs/encyclopedia/workflow/workflow-execution/workflowid-runid.mdx +++ b/docs/encyclopedia/workflow/workflow-execution/workflowid-runid.mdx @@ -42,7 +42,7 @@ A Run Id uniquely identifies a Workflow Execution even if it shares a Workflow I ### Which operations lead to non-determinism issues? {/* #run-id-non-determinism */} -An operation like `ContinueAsNew`, `Retry`, `Cron`, and `Reset` creates a [Workflow Execution Chain](/workflow-execution#workflow-execution-chain) as identified by the [`first_execution_run_id`](https://github.com/temporalio/api/blob/master/temporal/api/history/v1/message.proto). +An operation like `ContinueAsNew`, `Retry`, `Cron`, and `Reset` creates a [Workflow Execution Chain](/workflow-execution#workflow-execution-chain) as identified by the [`first_execution_run_id`](https://github.com/temporalio/api/blob/main/temporal/api/history/v1/message.proto). Each operation creates a new Workflow Execution inside a chain run and saves its information as `first_execution_run_id`. Thus, the Run Id is updated during each operation on a Workflow Execution. @@ -64,7 +64,7 @@ Because of this behavior, you shouldn't rely on the current Run Id in your code For more information, see the following link. -- [`message.proto`](https://github.com/temporalio/api/blob/master/temporal/api/history/v1/message.proto#L75-L82) +- [`message.proto`](https://github.com/temporalio/api/blob/main/temporal/api/history/v1/message.proto#L75-L82) ## What is a Workflow Id? {/* #workflow-id */} diff --git a/docs/evaluate/temporal-cloud/overview.mdx b/docs/evaluate/temporal-cloud/overview.mdx index 626488b9b5..8e6cd3d8e9 100644 --- a/docs/evaluate/temporal-cloud/overview.mdx +++ b/docs/evaluate/temporal-cloud/overview.mdx @@ -27,7 +27,7 @@ The platform stores encrypted Workflow state and orchestrates execution, while y ## How Temporal Cloud works -Temporal Cloud operates as the control plane for your distributed applications: +Temporal Cloud operates as the Control Plane for your distributed applications: 1. **Your environment**: You run Workers that execute your Workflow and Activity code. These can be deployed anywhere—Kubernetes, VMs, serverless, on-premises. 2. **Temporal Cloud**: Manages Workflow state, Event History, task queuing, and scheduling. All data is encrypted in transit and at rest. @@ -58,7 +58,7 @@ This design limits blast radius and enables independent scaling. **Data plane**: Where your Workflows execute. Each cell processes Workflow operations, persists state, and manages task queues. The data plane is optimized for low latency and high throughput. -**Control plane**: Manages provisioning, configuration, and lifecycle operations. When you create a Namespace, the control plane: +**Control plane**: Manages provisioning, configuration, and lifecycle operations. When you create a Namespace, the Control Plane: 1. Selects an appropriate cell in your chosen region 2. Provisions database resources and roles 3. Generates and deploys mTLS certificates diff --git a/docs/production-deployment/self-hosted-guide/archival.mdx b/docs/production-deployment/self-hosted-guide/archival.mdx index 85a18a0812..73b93f4449 100644 --- a/docs/production-deployment/self-hosted-guide/archival.mdx +++ b/docs/production-deployment/self-hosted-guide/archival.mdx @@ -178,7 +178,7 @@ Setting the retention period to 0 results in the error _A valid retention period ::: Next, run a sample Workflow such as the -[helloworld temporal sample](https://github.com/temporalio/temporal-go-samples/tree/master/helloworld). +[helloworld temporal sample](https://github.com/temporalio/samples-go/tree/main/helloworld). When the Workflow Execution closes, Temporal schedules archival processing. diff --git a/docs/references/configuration.mdx b/docs/references/configuration.mdx index 4614a891d8..db8ad2228a 100644 --- a/docs/references/configuration.mdx +++ b/docs/references/configuration.mdx @@ -133,7 +133,7 @@ The `server` contains the following parameters: :::tip -See the [server samples repo](https://github.com/temporalio/samples-server/tree/master/tls) for sample TLS configurations. +See the [server samples repo](https://github.com/temporalio/samples-server/tree/main/tls) for sample TLS configurations. ::: diff --git a/docs/references/sdk-metrics.mdx b/docs/references/sdk-metrics.mdx index 6288bddb28..906ca34d79 100644 --- a/docs/references/sdk-metrics.mdx +++ b/docs/references/sdk-metrics.mdx @@ -46,8 +46,8 @@ PHP and Go metrics are defined in the Go SDK. Java metrics are defined in the Java SDK. Metrics are defined in the following locations. -- [Core SDK Worker metrics](https://github.com/temporalio/sdk-core/blob/master/crates/sdk-core/src/telemetry/metrics.rs) -- [Core SDK Client metrics](https://github.com/temporalio/sdk-core/blob/master/crates/client/src/metrics.rs) +- [Core SDK Worker metrics](https://github.com/temporalio/sdk-rust/blob/main/crates/sdk-core/src/telemetry/metrics.rs) +- [Core SDK Client metrics](https://github.com/temporalio/sdk-rust/blob/main/crates/client/src/metrics.rs) - [Java SDK Worker metrics](https://github.com/temporalio/sdk-java/blob/master/temporal-sdk/src/main/java/io/temporal/worker/MetricsType.java) - [Java SDK Client metrics](https://github.com/temporalio/sdk-java/blob/master/temporal-serviceclient/src/main/java/io/temporal/serviceclient/MetricsType.java) - [Go SDK Worker and Client metrics](https://github.com/temporalio/sdk-go/blob/c32b04729cc7691f80c16f80eed7f323ee5ce24f/internal/common/metrics/constants.go) diff --git a/docusaurus.config.js b/docusaurus.config.js index 9f742ea3bd..e4d42d1bb5 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -136,7 +136,7 @@ module.exports = async function createConfigAsync() { }, { label: 'Twitter', - href: 'https://twitter.com/temporalio', + href: 'https://x.com/temporalio', }, { label: 'YouTube', @@ -144,7 +144,7 @@ module.exports = async function createConfigAsync() { }, { label: 'About the docs', - href: 'https://github.com/temporalio/documentation/blob/master/README.md', + href: 'https://github.com/temporalio/documentation/blob/main/README.md', }, ], }, @@ -158,10 +158,6 @@ module.exports = async function createConfigAsync() { label: 'Meetups', href: 'https://temporal.io/community#events', }, - { - label: 'Workshops', - href: 'https://temporal.io/community#workshops', - }, { label: 'Support forum', href: 'https://community.temporal.io/', @@ -184,7 +180,7 @@ module.exports = async function createConfigAsync() { }, { label: 'Use cases', - href: 'https://temporal.io/use-cases', + href: 'https://temporal.io/in-use', }, { label: 'Newsletter signup', @@ -200,11 +196,11 @@ module.exports = async function createConfigAsync() { }, { label: 'Privacy policy', - to: 'https://temporal.io/global-privacy-policy', + href: 'https://temporal.io/global-privacy-policy', }, { label: 'Terms of service', - href: 'https://docs.temporal.io/pdf/temporal-tos-2021-07-24.pdf', + href: 'https://temporal.io/terms-of-service', }, { label: "We're hiring", diff --git a/sidebars.js b/sidebars.js index 79d3fbd669..008bb0a033 100644 --- a/sidebars.js +++ b/sidebars.js @@ -326,6 +326,7 @@ module.exports = { items: [ 'develop/java/nexus/quickstart', 'develop/java/nexus/feature-guide', + 'develop/java/nexus/standalone-operations', ], }, { @@ -621,6 +622,7 @@ module.exports = { 'develop/python/integrations/braintrust', 'develop/python/integrations/langgraph', 'develop/python/integrations/langsmith', + 'develop/python/integrations/strands-agents', ], }, ], diff --git a/src/components/elements/SdkSvgs/RustBlock.js b/src/components/elements/SdkSvgs/RustBlock.js index 184b09c4e0..5d58f926b8 100644 --- a/src/components/elements/SdkSvgs/RustBlock.js +++ b/src/components/elements/SdkSvgs/RustBlock.js @@ -7,27 +7,13 @@ const RustBlock = () => { className={styles.sdkRust} fill="none" height="40" - viewBox="0 0 40 40" + viewBox="0 0 24 24" width="40" xmlns="http://www.w3.org/2000/svg" > - - - - - - + + + ); }; diff --git a/src/components/elements/SdkSvgs/sdk-svg.module.css b/src/components/elements/SdkSvgs/sdk-svg.module.css index a7b8251237..dd078fcaae 100644 --- a/src/components/elements/SdkSvgs/sdk-svg.module.css +++ b/src/components/elements/SdkSvgs/sdk-svg.module.css @@ -89,7 +89,6 @@ } .sdkRust { - border: 1px solid #a52c00; &:hover { .backgroundPath { @@ -98,7 +97,6 @@ } .backgroundPath { - fill: #a52c00; transition: opacity 0.3s ease; } } diff --git a/src/theme/Admonition/index.js b/src/theme/Admonition/index.js index 247e58a509..8c105681c8 100644 --- a/src/theme/Admonition/index.js +++ b/src/theme/Admonition/index.js @@ -1,8 +1,8 @@ -import React from "react"; -import clsx from "clsx"; -import { ThemeClassNames } from "@docusaurus/theme-common"; -import Translate from "@docusaurus/Translate"; -import styles from "./styles.module.css"; +import React from 'react'; +import clsx from 'clsx'; +import { ThemeClassNames } from '@docusaurus/theme-common'; +import Translate from '@docusaurus/Translate'; +import styles from './styles.module.css'; function NoteIcon() { return ( @@ -108,7 +108,7 @@ function CopyCodeIcon() { // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style const AdmonitionConfigs = { note: { - infimaClassName: "secondary", + infimaClassName: 'secondary', iconComponent: NoteIcon, label: ( @@ -117,7 +117,7 @@ const AdmonitionConfigs = { ), }, tip: { - infimaClassName: "success", + infimaClassName: 'success', iconComponent: TipIcon, label: ( @@ -126,7 +126,7 @@ const AdmonitionConfigs = { ), }, danger: { - infimaClassName: "danger", + infimaClassName: 'danger', iconComponent: DangerIcon, label: ( @@ -147,7 +147,7 @@ const AdmonitionConfigs = { ), }, caution: { - infimaClassName: "warning", + infimaClassName: 'warning', iconComponent: CautionIcon, label: ( React.isValidElement(item) && item.props?.mdxType === "mdxAdmonitionTitle" + (item) => React.isValidElement(item) && item.props?.mdxType === 'mdxAdmonitionTitle' ); const rest = <>{items.filter((item) => item !== mdxAdmonitionTitle)}; return { @@ -231,7 +232,7 @@ export default function Admonition(props) { className={clsx( ThemeClassNames.common.admonition, ThemeClassNames.common.admonitionType(props.type), - "alert", + 'alert', `alert--${typeConfig.infimaClassName}`, styles.admonition )} diff --git a/static/img/assets/banner-rust-temporal.png b/static/img/assets/banner-rust-temporal.png index 9bb0227ea8..d552a92530 100644 Binary files a/static/img/assets/banner-rust-temporal.png and b/static/img/assets/banner-rust-temporal.png differ diff --git a/static/img/sdks/svgs/rust.svg b/static/img/sdks/svgs/rust.svg index 2a2c01c0f6..c4a5099ea0 100644 --- a/static/img/sdks/svgs/rust.svg +++ b/static/img/sdks/svgs/rust.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + +