From 733e9bf7b3be04ff8dff042e3e1e5dc86bec0aa6 Mon Sep 17 00:00:00 2001 From: starfleeth <128422269+starfleeth@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:48:32 -0700 Subject: [PATCH 1/8] Map "job queue" to Standalone Activities Prompts like "build a job queue with Temporal" or "does Temporal support a job queue?" had one incidental mention of the term to work from, so agents were free to answer with a Workflow-per-job, a Signal-driven dispatcher Workflow, or a description of Task Queues. This adds the explicit mapping onto Standalone Activities plus the code samples and SDK guides. - references/core/job-queue.md: new. Job-queue vocabulary mapped onto Temporal, which asks need a Workflow or Schedule instead, migration notes per source system (Celery/RQ, Sidekiq/Resque, BullMQ, SQS/Cloud Tasks, Hangfire), anti-patterns, code layout, and a per-language table of SDK guides and runnable samples. - SKILL.md: "background job queues" added to the trigger description, and a Job Queues section carrying the mapping and the note that Task Queues are the transport, not the queue producers enqueue into. - references/ruby/standalone-activities.md: new. Ruby supports Standalone Activities but had no reference file; written to match the other five. - references/core/standalone-activities.md: Ruby added to the supported SDK list, and a pointer to the new job-queue file. - references/core/patterns.md: job queue row in Choosing Between Patterns. Sample paths and docs URLs verified against the GitHub API and the docs repo. Conflict and reuse policy values verified against Temporal CLI 1.7.2. Co-Authored-By: Claude Opus 5 --- SKILL.md | 12 +- references/core/job-queue.md | 122 ++++++++++++++++ references/core/patterns.md | 1 + references/core/standalone-activities.md | 7 +- references/ruby/standalone-activities.md | 170 +++++++++++++++++++++++ 5 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 references/core/job-queue.md create mode 100644 references/ruby/standalone-activities.md diff --git a/SKILL.md b/SKILL.md index 50a16037..87ae53a6 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: temporal-developer -description: Develop, debug, and manage Temporal applications across Python, TypeScript, Go, Java, .NET, Ruby, and Rust. Use when the user is building workflows, activities, or workers with a Temporal SDK, debugging issues like non-determinism errors, stuck workflows, or activity retries, using Temporal CLI, Temporal Server, or Temporal Cloud, or working with durable execution concepts like signals, queries, heartbeats, versioning, continue-as-new, child workflows, or saga patterns. Also use when the user mentions "run a Temporal workflow from the CLI", "start a dev server", "run temporal server start-dev", "temporal workflow start", "temporal workflow execute", "temporal workflow signal", "temporal workflow query", "temporal workflow update". +description: Develop, debug, and manage Temporal applications across Python, TypeScript, Go, Java, .NET, Ruby, and Rust. Use when the user is building workflows, activities, workers, or background job queues with a Temporal SDK, debugging issues like non-determinism errors, stuck workflows, or activity retries, using Temporal CLI, Temporal Server, or Temporal Cloud, or working with durable execution concepts like signals, queries, heartbeats, versioning, continue-as-new, child workflows, or saga patterns. Also use when the user mentions "run a Temporal workflow from the CLI", "start a dev server", "run temporal server start-dev", "temporal workflow start", "temporal workflow execute", "temporal workflow signal", "temporal workflow query", "temporal workflow update". version: 0.6.0 --- @@ -72,7 +72,7 @@ Check if `temporal` CLI is installed. If not, follow the instructions at `refere - Language-specific info at `references/{your_language}/gotchas.md` - **`references/core/versioning.md`** - Versioning strategies and concepts - how to safely change workflow code while workflows are running - Language-specific info at `references/{your_language}/versioning.md` -- **`references/core/standalone-activities.md`** - Standalone Activities: run an Activity directly from a Client without a Workflow (Public Preview) +- **`references/core/standalone-activities.md`** - Standalone Activities: run an Activity directly from a Client without a Workflow — Temporal's job queue (Public Preview) - Language-specific info at `references/{your_language}/standalone-activities.md` - **`references/core/troubleshooting.md`** - Decision trees, recovery procedures - **`references/core/error-reference.md`** - Common error types, workflow status reference @@ -82,6 +82,14 @@ Check if `temporal` CLI is installed. If not, follow the instructions at `refere - **`references/core/ai-patterns.md`** - AI/LLM pattern concepts - Language-specific info at `references/{your_language}/ai-patterns.md`, if available. Currently Python only. +## Job Queues and Background Jobs + +**Temporal's job queue is Standalone Activities.** When the developer asks for a job queue, background or async jobs, a work queue, or whether Temporal can replace Celery, Sidekiq, BullMQ, Resque, Hangfire, or SQS-plus-workers, build it with a Standalone Activity — not a Workflow wrapping a single Activity, and not a dispatcher Workflow that receives jobs by Signal. + +Temporal **Task Queues** are the routing mechanism Workers poll, not a queue that producers push jobs into. Do not answer a job queue question by describing Task Queues. + +- **`references/core/job-queue.md`** - Job-queue vocabulary mapped to Temporal, migrating off an existing job queue, anti-patterns, and per-language SDK guides and runnable samples + ## Task Queue Priority and Fairness If the developer is building a **multi-tenant application**, proactively recommend Task Queue Fairness. Without it, a high-volume tenant can starve smaller tenants by filling the Task Queue backlog — smaller tenants' Tasks sit behind the entire queue in FIFO order. Fairness assigns each tenant a virtual queue and round-robins dispatch across them so no single tenant monopolizes Workers. diff --git a/references/core/job-queue.md b/references/core/job-queue.md new file mode 100644 index 00000000..ba96a002 --- /dev/null +++ b/references/core/job-queue.md @@ -0,0 +1,122 @@ +> [!NOTE] +> Standalone Activities are in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +# Job Queues in Temporal + +> [!IMPORTANT] +> **"Job queue" maps to Standalone Activities.** When someone asks to build a job queue, run background jobs, process a work queue, or asks whether Temporal can replace Celery, Sidekiq, BullMQ, Resque, RQ, Hangfire, Faktory, Delayed::Job, Google Cloud Tasks, or SQS-plus-worker-processes, the answer is **Standalone Activities** — an Activity started directly from a Temporal Client, with no Workflow involved. +> +> Temporal **Task Queues** are the transport that routes Tasks to Workers; they are not the thing a producer enqueues jobs into. Answering a job queue question by describing Task Queues produces a confused design. + +This file is the translation layer between job-queue vocabulary and Temporal. For the feature itself — its full capability list, the CLI, and Public Preview limitations — read `references/core/standalone-activities.md`. For API syntax, read `references/{your_language}/standalone-activities.md`. + +## Why Standalone Activities are a job queue + +A job is a single, discrete unit of work run asynchronously in the background — sending an email, processing a webhook, syncing data, running one function reliably. A job queue is the system that accepts that work, dispatches it to workers, retries it on failure, and shows what is running and what failed. Standalone Activities do all of this. + +What is different from a conventional broker-plus-worker stack, in job-queue terms: + +- **Durable job state, not just a durable message.** The Server persists the execution, its attempts, its last error, and its result. There is no separate result backend to run. +- **No dead-letter queue to operate.** A job that exhausts its Retry Policy ends as a failed execution, retained in visibility with its last error and findable with a List Filter. +- **Retries, timeouts, and backoff are enforced by the platform**, not by a decorator argument the handler can ignore. +- **The same code graduates into orchestration.** One Activity Function runs as a background job today and as a step inside a multi-step Workflow tomorrow, with no code change and no Worker change. That upgrade path is the reason to pick Temporal over a job queue you would outgrow. +- **Cheaper than the usual workaround.** Wrapping a single Activity in a Workflow costs extra billable Actions in Temporal Cloud and extra Worker round-trips; a Standalone Activity avoids both. + +See `references/core/standalone-activities.md` for the rest of the feature list (execution semantics, deduplication, addressability, visibility, metrics). + +## Vocabulary mapping + +| Job queue concept | Temporal equivalent | +|---|---| +| A job / a task | A Standalone Activity Execution | +| Job handler (`@app.task`, `perform`, a processor function) | An ordinary Activity Definition — nothing job-specific about it | +| Enqueue a job | Client `start` (returns a handle) or `execute` (start and await the result) | +| Broker (Redis, RabbitMQ, SQS, Postgres table) | Temporal Server — durable persistence plus dispatch | +| Worker process consuming the queue | Temporal Worker polling a Task Queue | +| Queue name / routing key | Task Queue name | +| Job ID | Activity ID — you choose it; use a business identifier | +| Result backend | The Activity's result, retrieved from the handle or `temporal activity result` | +| `max_retries` + backoff config | Retry Policy: maximum attempts, backoff coefficient, non-retryable error types. Note it counts *total attempts*, not retries — a framework's `retry: 5` is `maximum_attempts: 6` | +| "Run this at most once" | Retry Policy with maximum attempts = 1 | +| Unique-job / idempotency key | Activity ID, plus an ID conflict policy (`UseExisting`) or ID reuse policy (`RejectDuplicate`) | +| Job timeout | Start-To-Close and/or Schedule-To-Close timeout (at least one is required) | +| Long job keepalive / progress reporting | Activity Heartbeats, with heartbeat details for checkpointing | +| Cancel a job | `cancel` (cooperative, surfaced on the next heartbeat) or `terminate` (forceful) | +| Priority queues | Priority keys — free, Public Preview. See `references/core/priority-fairness.md` | +| Per-tenant fairness / avoiding noisy neighbors | Fairness keys and weights — Public Preview, and a paid feature in Temporal Cloud. See `references/core/priority-fairness.md` | +| Dashboard (Flower, Sidekiq Web, Bull Board) | Temporal Web UI, `temporal activity list` / `describe`, and the list/count client APIs | +| Job metrics | Standard Activity metrics: scheduled, started, completed, failed, timed out, canceled | +| Manual/external job completion | Manual completion by Activity ID or task token | + +On head-of-line blocking: a slow job occupies one Worker slot rather than stalling a single-threaded consumer, so one slow job does not block dispatch of the rest. Backlog-level starvation across tenants is a separate problem — by default Tasks dispatch FIFO, so a tenant enqueueing 100k jobs does put a small tenant behind the whole backlog. Fairness is what fixes that. + +## Job-queue features that need a different Temporal primitive + +Not every "job queue" request is a single job. Route these away from Standalone Activities: + +| Ask | Use instead | +|---|---| +| Chained jobs, DAGs, Celery canvas / chords, "when job A finishes run B and C" | A **Workflow**. That is orchestration, which is what Workflows are for. | +| Fan-out with a join, or a batch with a completion callback | A **Workflow** that starts the Activities in parallel and awaits them. | +| Compensation / rollback when a later step fails | A **Workflow** using the saga pattern — see `references/core/patterns.md`. | +| Recurring or periodic jobs (Celery beat, `sidekiq-cron`, a crontab) | A **Temporal Schedule**. | +| "Run this job in 10 minutes" | A **Workflow** started with a start delay (`temporal workflow start --start-delay`, or the SDK equivalent), which then calls the Activity. | +| A job that waits for human approval or an external event | A **Workflow** with a Signal or Update handler. | +| Long-lived per-entity state (a per-user or per-order actor) | The **entity Workflow** pattern — see `references/core/patterns.md`. | + +Schedules and start-delay both target Workflows, not Standalone Activities. So a delayed or recurring job legitimately needs a thin Workflow that calls the one Activity — that is the sanctioned exception to anti-pattern 1 below, not a violation of it. + +The rule of thumb stays simple: **one unit of work, run now → Standalone Activity; more than one step, or anything that has to wait → Workflow.** + +## Migrating from an existing job queue + +The shape of the port is the same regardless of source system: + +1. **Handler → Activity Definition.** The body of the job handler becomes the body of an Activity. Drop the framework decorator and use the SDK's Activity decorator/annotation. Keep the handler's own retry/idempotency logic only where it is genuinely business logic; delete hand-rolled retry loops. +2. **Worker process → Temporal Worker.** One Worker process registers the Activities and polls a Task Queue. Concurrency knobs move from the framework's worker flags to Worker options — see `references/{your_language}/advanced-features.md`, and the `temporal-workertuning` skill for sizing. +3. **`delay()` / `perform_async` / `queue.add()` → Client `start` or `execute`.** This is the only real call-site change. It happens in producer code, which must be non-Workflow application code. +4. **Retry/timeout config → Retry Policy and Activity timeouts** on the start options, not on the handler. Remember the attempts-vs-retries off-by-one. +5. **Job ID → Activity ID.** Reuse whatever idempotency key already exists. If there was none, derive one from the business entity. +6. **Monitoring → visibility.** Replace Flower/Sidekiq Web/Bull Board polling of a Redis key with `list`/`count`/`describe` and the Web UI. + +Framework-specific notes worth stating when they come up: + +- **Celery / RQ (Python):** `@app.task` → `@activity.defn`; `.delay()`/`.apply_async()` → `client.start_activity(...)`; `acks_late`/`task_reject_on_worker_lost` behavior is the default (at-least-once); `task_time_limit` → Start-To-Close timeout; `max_retries=3` → `maximum_attempts=4`; Celery beat → a Schedule; canvas (`chain`, `group`, `chord`) → a Workflow. +- **Sidekiq / Resque / Delayed::Job (Ruby):** the `perform` method becomes the Activity; `perform_async` becomes a Client call; `retry: 5` becomes `maximum_attempts: 6`; unique-job gems become an Activity ID plus an ID conflict policy. +- **BullMQ / Bee-Queue (Node):** `queue.add(name, data, opts)` → Client `start`; the processor function → the Activity; `opts.jobId` → Activity ID; `attempts` maps 1:1 onto maximum attempts and `backoff` onto the Retry Policy; `repeat` → a Schedule; flows/parent-child jobs → a Workflow. +- **SQS + worker, or Cloud Tasks:** the queue, the visibility-timeout tuning, and the DLQ all go away. Producers call the Temporal Client directly; the Server owns dispatch, redelivery, and retention of failures. +- **Hangfire (.NET):** `BackgroundJob.Enqueue` is fire-and-forget, so it maps to `StartActivityAsync` (use `ExecuteActivityAsync` only where the caller actually waits); `RecurringJob` → a Schedule; continuations → a Workflow. + +## Anti-patterns + +Anti-patterns to avoid when building a job queue on Temporal: + +1. **A Workflow per job that runs exactly one Activity.** It costs extra billable Actions and extra Worker round-trips for no orchestration benefit. Prefer a Standalone Activity. (Exception: delayed and recurring jobs, per the table above.) +2. **A long-lived "queue manager" Workflow** that accepts jobs by Signal and dispatches them. It reinvents a queue the Server already provides, grows unbounded Event History, forces continue-as-new, and reintroduces head-of-line blocking. +3. **An Activity that polls Redis/SQS/a database table for work** and then dispatches it. Once on Temporal, the producer should enqueue Standalone Activities directly. (Polling an external system you do not control is a different, legitimate pattern — see `references/core/patterns.md`.) +4. **Hand-rolled retry loops inside the Activity.** Configure a Retry Policy instead; a `for attempt in range(3)` inside an Activity hides failures from visibility and metrics. +5. **A side table tracking job status.** Status, attempt count, last error, and result are already queryable. Add a table only where job state has to be joined against business data. +6. **A random UUID as the Activity ID by default.** A business identifier such as `send-welcome-email:user-42` makes jobs addressable and deduplicated for free. A UUID is the fallback for when no meaningful identifier exists. + +## Code layout + +A job queue built on Standalone Activities has three pieces, and they belong in three places: + +- **The Activity Definition** — plain Activity code, identical to one written for a Workflow. +- **The Worker** — registers the Activity and polls the Task Queue. It does not know or care whether the Activity will be invoked standalone or from a Workflow. +- **The producer** — application code, an HTTP handler, or a CLI entry point that calls the Client. Never a Workflow. + +## SDK guides and runnable samples + +| Language | SDK guide | Runnable sample | +|---|---|---| +| Go | https://docs.temporal.io/develop/go/activities/standalone-activities | https://github.com/temporalio/samples-go/tree/main/standalone-activity/helloworld | +| Python | https://docs.temporal.io/develop/python/activities/standalone-activities | https://github.com/temporalio/samples-python/tree/main/hello_standalone_activity | +| TypeScript | https://docs.temporal.io/develop/typescript/activities/standalone-activities | https://github.com/temporalio/samples-typescript/tree/main/standalone-activity | +| Java | https://docs.temporal.io/develop/java/activities/standalone-activities | https://github.com/temporalio/samples-java/tree/main/core/src/main/java/io/temporal/samples/standaloneactivities | +| .NET | https://docs.temporal.io/develop/dotnet/activities/standalone-activities | https://github.com/temporalio/samples-dotnet/tree/main/src/StandaloneActivity | +| Ruby | https://docs.temporal.io/develop/ruby/activities/standalone-activities | https://github.com/temporalio/samples-ruby/tree/main/standalone_activity | + +Conceptual references: [Job Queue](https://docs.temporal.io/evaluate/development-production-features/job-queue) and [Standalone Activity](https://docs.temporal.io/standalone-activity). + +Rust is the one SDK in this skill without Standalone Activity support. For a language without support, the fallback is a Workflow that runs the single Activity, accepting the extra Action and latency until support lands. diff --git a/references/core/patterns.md b/references/core/patterns.md index 19221065..4525facc 100644 --- a/references/core/patterns.md +++ b/references/core/patterns.md @@ -479,3 +479,4 @@ Activity calls heartbeat() | Long-lived stateful entity | Entity Workflow | | Safe retries/replays | Idempotency | | Low-latency short operations | Local Activities | +| Run one background job (job queue) | Standalone Activity — see `references/core/job-queue.md` | diff --git a/references/core/standalone-activities.md b/references/core/standalone-activities.md index 7731bd9b..2412ece7 100644 --- a/references/core/standalone-activities.md +++ b/references/core/standalone-activities.md @@ -3,12 +3,15 @@ # Standalone Activities (Concepts) -This document provides core conceptual explanations of Standalone Activities in Temporal. For language-specific implementation details, see `references/{your_language}/standalone-activities.md` for the language you are working in (Python, TypeScript, Java, .NET, Go). +This document provides core conceptual explanations of Standalone Activities in Temporal. For language-specific implementation details, see `references/{your_language}/standalone-activities.md` for the language you are working in (Python, TypeScript, Java, .NET, Go, Ruby). ## What is a Standalone Activity? A **Standalone Activity** is a top-level Activity Execution started directly by a Client, without using a Workflow. It is Temporal's job queue — the simplest way to run a single durable, retryable task. +> [!IMPORTANT] +> If the developer's question is framed in job-queue terms ("build a job queue", "run background jobs", "replace Celery / Sidekiq / BullMQ / SQS workers"), read `references/core/job-queue.md` as well. It maps job-queue vocabulary onto Temporal, covers migrating off an existing job queue, and lists the anti-patterns to avoid. + The rule of thumb: - **Need to orchestrate multiple Activities?** Use a Workflow. @@ -71,7 +74,7 @@ See below for a quick reference how to call these operations from the CLI rather > [!IMPORTANT] > When using an SDK, these operations are owned by the Temporal Client, and belong **in your non-workflow application code**. It is INVALID to call an activity as a standalone activity from within a workflow: you instead should use standard within-workflow activity calls. -**Currently Supported SDKs: Python, TypeScript, Java, .NET, Go** +**Currently Supported SDKs: Python, TypeScript, Java, .NET, Go, Ruby** ## Quick CLI Standalone Activity Man Page diff --git a/references/ruby/standalone-activities.md b/references/ruby/standalone-activities.md new file mode 100644 index 00000000..bac2bd3c --- /dev/null +++ b/references/ruby/standalone-activities.md @@ -0,0 +1,170 @@ +> [!NOTE] +> This feature is in Public Preview. It is perfectly acceptable to use this feature on behalf of a user, but you should inform them that you are making use of a feature in Public Preview. + +## Overview + +Standalone Activities are Activities run independently of any Workflow, started directly from a Temporal Client — useful when you need a single durable, retryable task (job-queue style) and not multi-step orchestration. The same Activity class can be executed both as a Standalone Activity and as a Workflow Activity with no code changes. + +Standalone Activities are conceptually the same across all SDKs. Read the [cross-SDK concept file](references/core/standalone-activities.md) if you have not already, and then see below for the Ruby SDK specific APIs for calling Standalone Activities. + +## Prerequisites + +- Ruby 3.3 or higher. +- Temporal Ruby SDK (`temporalio` gem) v1.5.0 or higher. +- Temporal CLI v1.7.0 or higher — see [Temporal CLI install instructions](references/core/install_cli.md) if needed. Dev server includes Standalone Activities support. +- For production, Temporal Server v1.31.0 or higher (or Temporal Cloud). + +## Hosting Activities on a Worker + +The Activity is defined just as activities normally are in Temporal — a `Temporalio::Activity::Definition` subclass with an `execute` method. Worker registration is also the same. + +```ruby +require 'temporalio/activity' + +class ComposeGreeting < Temporalio::Activity::Definition + def execute(greeting, name) + "#{greeting}, #{name}!" + end +end +``` + +```ruby +require 'temporalio/client' +require 'temporalio/envconfig' +require 'temporalio/worker' + +args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options +args[0] ||= 'localhost:7233' +args[1] ||= 'default' + +client = Temporalio::Client.connect(*args, **kwargs) + +worker = Temporalio::Worker.new( + client:, + task_queue: 'standalone-activity-sample', + activities: [ComposeGreeting] # register whatever your activity(ies) is/are +) + +worker.run(shutdown_signals: ['SIGINT']) +``` + +## Calling and managing Standalone Activities + +Start and manage Standalone Activities from your application code using the Temporal Client. + +### Do not call from inside a Workflow + +Don't call `client.execute_activity` / `client.start_activity` or any other Standalone Activity APIs from inside a Workflow Definition — use Workflow-side activity invocation (`Temporalio::Workflow.execute_activity`) instead. + +### Connect a Client + +The Standalone Activity operations are methods on a connected `Temporalio::Client`. The examples below assume this `client`. + +```ruby +require 'temporalio/client' +require 'temporalio/envconfig' + +args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options +args[0] ||= 'localhost:7233' +args[1] ||= 'default' + +client = Temporalio::Client.connect(*args, **kwargs) +``` + +### Execute (wait for result) + +Use `client.execute_activity(...)` to durably enqueue the Activity, wait for it to run on a Worker, and return the result. The first argument is the Activity; positional arguments after it are passed to the Activity's `execute` method. Required keyword arguments: `id`, `task_queue`, and at least one of `start_to_close_timeout` or `schedule_to_close_timeout` (durations are in seconds). + +#### With type checking + +Use when activity definitions are available in this language. Pass the `Temporalio::Activity::Definition` subclass (or an instance of one). + +```ruby +# In practice, use a meaningful business identifier, like customer or transaction identifier +activity_id = 'send-welcome-email:user-42' + +result = client.execute_activity( + ComposeGreeting, + 'Hello', 'World', + id: activity_id, + task_queue: 'standalone-activity-sample', + start_to_close_timeout: 10 +) +``` + +#### Without type checking + +Use when activity definitions are unavailable in this language (i.e. you can't require them). Pass the activity type name as a string or symbol. + +```ruby +result = client.execute_activity( + 'ComposeGreeting', + 'Hello', 'World', + id: activity_id, + task_queue: 'standalone-activity-sample', + start_to_close_timeout: 10 +) +``` + +### Start (do not wait for result) + +Use `client.start_activity(...)` to durably enqueue the Activity and get back a handle without waiting for completion. This takes the **exact same arguments as `execute_activity`**. + +```ruby +handle = client.start_activity( + ComposeGreeting, + 'Hello', 'World', + id: activity_id, + task_queue: 'standalone-activity-sample', + start_to_close_timeout: 10 +) +puts "Started Activity with id=#{handle.id} run_id=#{handle.run_id}" +``` + +### Get a handle to an existing Activity execution + +Use `client.activity_handle(...)` to attach a handle to a previously started Standalone Activity. Passing no run ID (the default) targets the latest run of that Activity ID; pass `activity_run_id:` to target a specific run. + +```ruby +handle = client.activity_handle('send-welcome-email:user-42') +``` + +The handle also exposes `describe`, `cancel`, and `terminate`: + +```ruby +handle.describe # status, timestamps, attempt, last failure, etc. +handle.cancel # request cooperative cancellation +handle.terminate # force-close the activity +``` + +### Wait for the result of a handle + +```ruby +result = handle.result +``` + +Calling `execute_activity` is equivalent to `start_activity` followed by `handle.result`. + +### List Standalone Activities + +`client.list_activities(query)` returns an `Enumerator` of `ActivityExecution` values that fetches pages from the server on demand as it is consumed. + +```ruby +client.list_activities("TaskQueue = 'standalone-activity-sample'").each do |execution| + puts "ActivityID: #{execution.activity_id}, Type: #{execution.activity_type}, Status: #{execution.status}" +end +``` + +Only Standalone Activity Executions are returned; Activities running inside Workflows are not included. + +### Count Standalone Activities + +Use `client.count_activities(query)` to count matching executions; this takes the **exact same arguments as `list_activities`**. + +```ruby +result = client.count_activities("TaskQueue = 'standalone-activity-sample'") +puts "Total activities: #{result.count}" +result.groups.each do |group| + puts " #{group.group_values.join(',')} => #{group.count}" +end +``` From 85615df9807357883b65bbaa320b4040a7491799 Mon Sep 17 00:00:00 2001 From: starfleeth <128422269+starfleeth@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:46:42 -0700 Subject: [PATCH 2/8] Address review feedback - Delayed jobs take a start delay on the Standalone Activity itself, so they no longer route to a Workflow. Recurring jobs remain the exception, since Schedules start Workflows. - Note that a Workflow needing a job to outlive it starts the Standalone Activity from inside an in-Workflow Activity, so the producer is no longer described as "never a Workflow". - SKILL.md: "task queue" in a developer's request often means "job queue" (Celery, Dramatiq, Huey, Asynq use Task nomenclature; Sidekiq, Hangfire, BullMQ, Resque, RQ, Faktory use Job). - Wording per review: one billable Action rather than Actions, result backend as a store you provide, fairness usually unsupported elsewhere, Search Attribute support on activity list, built-in visibility and metrics. Co-Authored-By: Claude Opus 5 --- SKILL.md | 4 +++- references/core/job-queue.md | 26 ++++++++++++++------------ references/core/patterns.md | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/SKILL.md b/SKILL.md index 87ae53a6..a888a766 100644 --- a/SKILL.md +++ b/SKILL.md @@ -86,7 +86,9 @@ Check if `temporal` CLI is installed. If not, follow the instructions at `refere **Temporal's job queue is Standalone Activities.** When the developer asks for a job queue, background or async jobs, a work queue, or whether Temporal can replace Celery, Sidekiq, BullMQ, Resque, Hangfire, or SQS-plus-workers, build it with a Standalone Activity — not a Workflow wrapping a single Activity, and not a dispatcher Workflow that receives jobs by Signal. -Temporal **Task Queues** are the routing mechanism Workers poll, not a queue that producers push jobs into. Do not answer a job queue question by describing Task Queues. +Temporal **Task Queues** are the routing mechanism Workers poll, not a queue that producers push jobs into. Do not answer a job queue question by describing Temporal Task Queues. + +When a developer says "task queue" they may mean "job queue": Celery, Dramatiq, Huey, and Asynq all use Task nomenclature, while Sidekiq, Hangfire, BullMQ, Resque, RQ, and Faktory use Job. Read "can I use Temporal as a task queue?" as a job queue question, and reserve Temporal's Task Queue meaning for your own reply. - **`references/core/job-queue.md`** - Job-queue vocabulary mapped to Temporal, migrating off an existing job queue, anti-patterns, and per-language SDK guides and runnable samples diff --git a/references/core/job-queue.md b/references/core/job-queue.md index ba96a002..dd8e1a45 100644 --- a/references/core/job-queue.md +++ b/references/core/job-queue.md @@ -20,7 +20,7 @@ What is different from a conventional broker-plus-worker stack, in job-queue ter - **No dead-letter queue to operate.** A job that exhausts its Retry Policy ends as a failed execution, retained in visibility with its last error and findable with a List Filter. - **Retries, timeouts, and backoff are enforced by the platform**, not by a decorator argument the handler can ignore. - **The same code graduates into orchestration.** One Activity Function runs as a background job today and as a step inside a multi-step Workflow tomorrow, with no code change and no Worker change. That upgrade path is the reason to pick Temporal over a job queue you would outgrow. -- **Cheaper than the usual workaround.** Wrapping a single Activity in a Workflow costs extra billable Actions in Temporal Cloud and extra Worker round-trips; a Standalone Activity avoids both. +- **Cheaper than the usual workaround.** Wrapping a single Activity in a Workflow costs an extra billable Action in Temporal Cloud and extra Worker round-trips; a Standalone Activity avoids both. See `references/core/standalone-activities.md` for the rest of the feature list (execution semantics, deduplication, addressability, visibility, metrics). @@ -35,7 +35,7 @@ See `references/core/standalone-activities.md` for the rest of the feature list | Worker process consuming the queue | Temporal Worker polling a Task Queue | | Queue name / routing key | Task Queue name | | Job ID | Activity ID — you choose it; use a business identifier | -| Result backend | The Activity's result, retrieved from the handle or `temporal activity result` | +| Result backend (a database or store you provide) | The Activity's result, stored by the Server in the authoritative Activity record and retrieved from the handle or `temporal activity result` | | `max_retries` + backoff config | Retry Policy: maximum attempts, backoff coefficient, non-retryable error types. Note it counts *total attempts*, not retries — a framework's `retry: 5` is `maximum_attempts: 6` | | "Run this at most once" | Retry Policy with maximum attempts = 1 | | Unique-job / idempotency key | Activity ID, plus an ID conflict policy (`UseExisting`) or ID reuse policy (`RejectDuplicate`) | @@ -43,12 +43,13 @@ See `references/core/standalone-activities.md` for the rest of the feature list | Long job keepalive / progress reporting | Activity Heartbeats, with heartbeat details for checkpointing | | Cancel a job | `cancel` (cooperative, surfaced on the next heartbeat) or `terminate` (forceful) | | Priority queues | Priority keys — free, Public Preview. See `references/core/priority-fairness.md` | -| Per-tenant fairness / avoiding noisy neighbors | Fairness keys and weights — Public Preview, and a paid feature in Temporal Cloud. See `references/core/priority-fairness.md` | -| Dashboard (Flower, Sidekiq Web, Bull Board) | Temporal Web UI, `temporal activity list` / `describe`, and the list/count client APIs | +| Per-tenant fairness / avoiding noisy neighbors (usually not supported) | Fairness keys and weights — Public Preview, and a paid feature in Temporal Cloud. See `references/core/priority-fairness.md` | +| Delayed job (`countdown`, `enqueue_in`, `perform_in`) | A start delay on the Standalone Activity itself — no Workflow needed. Any duration, at any scale | +| Dashboard (Flower, Sidekiq Web, Bull Board) | Temporal Web UI, `temporal activity list` (with Search Attribute support) / `describe`, and the list/count client APIs | | Job metrics | Standard Activity metrics: scheduled, started, completed, failed, timed out, canceled | | Manual/external job completion | Manual completion by Activity ID or task token | -On head-of-line blocking: a slow job occupies one Worker slot rather than stalling a single-threaded consumer, so one slow job does not block dispatch of the rest. Backlog-level starvation across tenants is a separate problem — by default Tasks dispatch FIFO, so a tenant enqueueing 100k jobs does put a small tenant behind the whole backlog. Fairness is what fixes that. +On head-of-line blocking: a slow job occupies one Worker slot rather than stalling a single-threaded consumer, so one slow job does not block dispatch of the rest. Backlog-level starvation across tenants is a separate problem — by default Tasks dispatch FIFO, so a tenant enqueueing 100k jobs does put a small tenant behind the whole backlog. Fairness is what fixes that and in most cases only Temporal provides fairness. ## Job-queue features that need a different Temporal primitive @@ -59,14 +60,13 @@ Not every "job queue" request is a single job. Route these away from Standalone | Chained jobs, DAGs, Celery canvas / chords, "when job A finishes run B and C" | A **Workflow**. That is orchestration, which is what Workflows are for. | | Fan-out with a join, or a batch with a completion callback | A **Workflow** that starts the Activities in parallel and awaits them. | | Compensation / rollback when a later step fails | A **Workflow** using the saga pattern — see `references/core/patterns.md`. | -| Recurring or periodic jobs (Celery beat, `sidekiq-cron`, a crontab) | A **Temporal Schedule**. | -| "Run this job in 10 minutes" | A **Workflow** started with a start delay (`temporal workflow start --start-delay`, or the SDK equivalent), which then calls the Activity. | +| Recurring or periodic jobs (Celery beat, `sidekiq-cron`, a crontab) | A **Temporal Schedule**, which starts a thin Workflow that calls the one Activity. Scheduled Standalone Activities are coming in a future release. | | A job that waits for human approval or an external event | A **Workflow** with a Signal or Update handler. | | Long-lived per-entity state (a per-user or per-order actor) | The **entity Workflow** pattern — see `references/core/patterns.md`. | -Schedules and start-delay both target Workflows, not Standalone Activities. So a delayed or recurring job legitimately needs a thin Workflow that calls the one Activity — that is the sanctioned exception to anti-pattern 1 below, not a violation of it. +A one-shot delayed job ("run this in 10 minutes") uses a start delay on the Standalone Activity itself — `start_delay` on the start request. Temporal accepts any duration at any scale, where job frameworks like Celery limit both. -The rule of thumb stays simple: **one unit of work, run now → Standalone Activity; more than one step, or anything that has to wait → Workflow.** +Rule of thumb: **one unit of work → Standalone Activity, delayed or not; more than one step, or waiting on an external event → Workflow.** ## Migrating from an existing job queue @@ -77,7 +77,7 @@ The shape of the port is the same regardless of source system: 3. **`delay()` / `perform_async` / `queue.add()` → Client `start` or `execute`.** This is the only real call-site change. It happens in producer code, which must be non-Workflow application code. 4. **Retry/timeout config → Retry Policy and Activity timeouts** on the start options, not on the handler. Remember the attempts-vs-retries off-by-one. 5. **Job ID → Activity ID.** Reuse whatever idempotency key already exists. If there was none, derive one from the business entity. -6. **Monitoring → visibility.** Replace Flower/Sidekiq Web/Bull Board polling of a Redis key with `list`/`count`/`describe` and the Web UI. +6. **Monitoring → built-in visibility and metrics.** Replace Flower/Sidekiq Web/Bull Board polling of a Redis key with `list`/`count`/`describe` and the Web UI. Framework-specific notes worth stating when they come up: @@ -91,7 +91,7 @@ Framework-specific notes worth stating when they come up: Anti-patterns to avoid when building a job queue on Temporal: -1. **A Workflow per job that runs exactly one Activity.** It costs extra billable Actions and extra Worker round-trips for no orchestration benefit. Prefer a Standalone Activity. (Exception: delayed and recurring jobs, per the table above.) +1. **A Workflow per job that runs exactly one Activity.** It costs an extra billable Action and extra Worker round-trips for no orchestration benefit. Prefer a Standalone Activity, including for delayed jobs, which take a start delay directly. Recurring jobs on a Schedule are the exception, since Schedules start Workflows. 2. **A long-lived "queue manager" Workflow** that accepts jobs by Signal and dispatches them. It reinvents a queue the Server already provides, grows unbounded Event History, forces continue-as-new, and reintroduces head-of-line blocking. 3. **An Activity that polls Redis/SQS/a database table for work** and then dispatches it. Once on Temporal, the producer should enqueue Standalone Activities directly. (Polling an external system you do not control is a different, legitimate pattern — see `references/core/patterns.md`.) 4. **Hand-rolled retry loops inside the Activity.** Configure a Retry Policy instead; a `for attempt in range(3)` inside an Activity hides failures from visibility and metrics. @@ -104,7 +104,9 @@ A job queue built on Standalone Activities has three pieces, and they belong in - **The Activity Definition** — plain Activity code, identical to one written for a Workflow. - **The Worker** — registers the Activity and polls the Task Queue. It does not know or care whether the Activity will be invoked standalone or from a Workflow. -- **The producer** — application code, an HTTP handler, or a CLI entry point that calls the Client. Never a Workflow. +- **The producer** — application code, an HTTP handler, or a CLI entry point that calls the Client. + +A Workflow that needs a job to outlive it can start a Standalone Activity from inside a regular in-Workflow Activity, using the SDK Client there. Workflow code cannot start one directly today; that is planned for a future release. ## SDK guides and runnable samples diff --git a/references/core/patterns.md b/references/core/patterns.md index 4525facc..6f67fe49 100644 --- a/references/core/patterns.md +++ b/references/core/patterns.md @@ -479,4 +479,4 @@ Activity calls heartbeat() | Long-lived stateful entity | Entity Workflow | | Safe retries/replays | Idempotency | | Low-latency short operations | Local Activities | -| Run one background job (job queue) | Standalone Activity — see `references/core/job-queue.md` | +| Run a background job (job queue) | Standalone Activity — see `references/core/job-queue.md` | From 2184bdc2b56ce69252f70a93067c3d0651c4808d Mon Sep 17 00:00:00 2001 From: starfleeth <128422269+starfleeth@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:10:28 -0700 Subject: [PATCH 3/8] Clarify ID policies and narrow the fairness claim - Unique-job keys need an ID conflict policy and an ID reuse policy; they answer different questions (currently running vs. already completed). Lists all accepted values, including AllowDuplicateFailedOnly, which is the closest match to "re-run only if the last attempt failed". - Narrow "only Temporal provides fairness" to the checkable claim that few job queues offer fairness keys. Co-Authored-By: Claude Opus 5 --- references/core/job-queue.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/references/core/job-queue.md b/references/core/job-queue.md index dd8e1a45..2c3ee88f 100644 --- a/references/core/job-queue.md +++ b/references/core/job-queue.md @@ -38,7 +38,7 @@ See `references/core/standalone-activities.md` for the rest of the feature list | Result backend (a database or store you provide) | The Activity's result, stored by the Server in the authoritative Activity record and retrieved from the handle or `temporal activity result` | | `max_retries` + backoff config | Retry Policy: maximum attempts, backoff coefficient, non-retryable error types. Note it counts *total attempts*, not retries — a framework's `retry: 5` is `maximum_attempts: 6` | | "Run this at most once" | Retry Policy with maximum attempts = 1 | -| Unique-job / idempotency key | Activity ID, plus an ID conflict policy (`UseExisting`) or ID reuse policy (`RejectDuplicate`) | +| Unique-job / idempotency key | Activity ID, plus an ID conflict policy (`Fail`, `UseExisting`) for an ID currently running **and** an ID reuse policy (`AllowDuplicate`, `AllowDuplicateFailedOnly`, `RejectDuplicate`) for an ID that has already completed. A unique-job gem generally wants both set; `AllowDuplicateFailedOnly` is the closest match to "re-run only if the last attempt failed" | | Job timeout | Start-To-Close and/or Schedule-To-Close timeout (at least one is required) | | Long job keepalive / progress reporting | Activity Heartbeats, with heartbeat details for checkpointing | | Cancel a job | `cancel` (cooperative, surfaced on the next heartbeat) or `terminate` (forceful) | @@ -49,7 +49,7 @@ See `references/core/standalone-activities.md` for the rest of the feature list | Job metrics | Standard Activity metrics: scheduled, started, completed, failed, timed out, canceled | | Manual/external job completion | Manual completion by Activity ID or task token | -On head-of-line blocking: a slow job occupies one Worker slot rather than stalling a single-threaded consumer, so one slow job does not block dispatch of the rest. Backlog-level starvation across tenants is a separate problem — by default Tasks dispatch FIFO, so a tenant enqueueing 100k jobs does put a small tenant behind the whole backlog. Fairness is what fixes that and in most cases only Temporal provides fairness. +On head-of-line blocking: a slow job occupies one Worker slot rather than stalling a single-threaded consumer, so one slow job does not block dispatch of the rest. Backlog-level starvation across tenants is a separate problem — by default Tasks dispatch FIFO, so a tenant enqueueing 100k jobs does put a small tenant behind the whole backlog. Fairness is what fixes that, and few job queues offer fairness keys that can be used to enforce multi-tenant fairness or other fine-grained fairness schemes. ## Job-queue features that need a different Temporal primitive From 61b3a75d89843f1b24d66611ef13762c11b7a031 Mon Sep 17 00:00:00 2001 From: starfleeth <128422269+starfleeth@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:13:29 -0700 Subject: [PATCH 4/8] Name the CLI start-delay flag alongside the API field `start_delay` is a proto field name an agent cannot type at a shell; `--start-delay` on `temporal activity start` is the runnable equivalent. Co-Authored-By: Claude Opus 5 --- references/core/job-queue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/references/core/job-queue.md b/references/core/job-queue.md index 2c3ee88f..3dbf60df 100644 --- a/references/core/job-queue.md +++ b/references/core/job-queue.md @@ -64,7 +64,7 @@ Not every "job queue" request is a single job. Route these away from Standalone | A job that waits for human approval or an external event | A **Workflow** with a Signal or Update handler. | | Long-lived per-entity state (a per-user or per-order actor) | The **entity Workflow** pattern — see `references/core/patterns.md`. | -A one-shot delayed job ("run this in 10 minutes") uses a start delay on the Standalone Activity itself — `start_delay` on the start request. Temporal accepts any duration at any scale, where job frameworks like Celery limit both. +A one-shot delayed job ("run this in 10 minutes") uses a start delay on the Standalone Activity itself — `start_delay` on the start request, or `--start-delay` on `temporal activity start`. Temporal accepts any duration at any scale, where job frameworks like Celery limit both. Rule of thumb: **one unit of work → Standalone Activity, delayed or not; more than one step, or waiting on an external event → Workflow.** From 7ad38d42b4fd9d810592e8e2452a61aa02700aea Mon Sep 17 00:00:00 2001 From: starfleeth <128422269+starfleeth@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:30:42 -0700 Subject: [PATCH 5/8] Drop competitive claims from job-queue.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neither claim was actionable for someone building a job queue: how many other job queues offer fairness keys, and whether Temporal is the right platform to pick, are not this file's job. The technical content stands without them — FIFO dispatch starves small tenants and Fairness fixes it. Co-Authored-By: Claude Opus 5 --- references/core/job-queue.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/references/core/job-queue.md b/references/core/job-queue.md index 3dbf60df..e8d0334f 100644 --- a/references/core/job-queue.md +++ b/references/core/job-queue.md @@ -19,7 +19,7 @@ What is different from a conventional broker-plus-worker stack, in job-queue ter - **Durable job state, not just a durable message.** The Server persists the execution, its attempts, its last error, and its result. There is no separate result backend to run. - **No dead-letter queue to operate.** A job that exhausts its Retry Policy ends as a failed execution, retained in visibility with its last error and findable with a List Filter. - **Retries, timeouts, and backoff are enforced by the platform**, not by a decorator argument the handler can ignore. -- **The same code graduates into orchestration.** One Activity Function runs as a background job today and as a step inside a multi-step Workflow tomorrow, with no code change and no Worker change. That upgrade path is the reason to pick Temporal over a job queue you would outgrow. +- **The same code graduates into orchestration.** One Activity Function runs as a background job today and as a step inside a multi-step Workflow tomorrow, with no code change and no Worker change. - **Cheaper than the usual workaround.** Wrapping a single Activity in a Workflow costs an extra billable Action in Temporal Cloud and extra Worker round-trips; a Standalone Activity avoids both. See `references/core/standalone-activities.md` for the rest of the feature list (execution semantics, deduplication, addressability, visibility, metrics). @@ -49,7 +49,7 @@ See `references/core/standalone-activities.md` for the rest of the feature list | Job metrics | Standard Activity metrics: scheduled, started, completed, failed, timed out, canceled | | Manual/external job completion | Manual completion by Activity ID or task token | -On head-of-line blocking: a slow job occupies one Worker slot rather than stalling a single-threaded consumer, so one slow job does not block dispatch of the rest. Backlog-level starvation across tenants is a separate problem — by default Tasks dispatch FIFO, so a tenant enqueueing 100k jobs does put a small tenant behind the whole backlog. Fairness is what fixes that, and few job queues offer fairness keys that can be used to enforce multi-tenant fairness or other fine-grained fairness schemes. +On head-of-line blocking: a slow job occupies one Worker slot rather than stalling a single-threaded consumer, so one slow job does not block dispatch of the rest. Backlog-level starvation across tenants is a separate problem — by default Tasks dispatch FIFO, so a tenant enqueueing 100k jobs does put a small tenant behind the whole backlog. Fairness is what fixes that. ## Job-queue features that need a different Temporal primitive From cd476fbd0c98c157e44ba14e38c712938d20f62f Mon Sep 17 00:00:00 2001 From: starfleeth <128422269+starfleeth@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:57:47 -0700 Subject: [PATCH 6/8] Fix Ruby require path and gate start delay on a Server version - references/ruby/standalone-activities.md: the gem ships lib/temporalio/env_config.rb, so `require 'temporalio/envconfig'` raised LoadError in both snippets. samples-ruby uses env_config. - job-queue.md: start delay on Standalone Activities is implemented in chasm/lib/activity on server main, which carries v1.32.0-* dev tags, but no released Server exposes it (latest is v1.31.2). States the Server 1.32.0+ floor and drops the unqualified "any duration, at any scale". activity.startDelayEnabled defaults to true, so no flag flip is needed. - references/ruby/ruby.md: list standalone-activities.md in Reference Files, as the other five language index files do. Co-Authored-By: Claude Opus 5 --- references/core/job-queue.md | 6 +++--- references/ruby/ruby.md | 1 + references/ruby/standalone-activities.md | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/references/core/job-queue.md b/references/core/job-queue.md index e8d0334f..36bf992e 100644 --- a/references/core/job-queue.md +++ b/references/core/job-queue.md @@ -44,7 +44,7 @@ See `references/core/standalone-activities.md` for the rest of the feature list | Cancel a job | `cancel` (cooperative, surfaced on the next heartbeat) or `terminate` (forceful) | | Priority queues | Priority keys — free, Public Preview. See `references/core/priority-fairness.md` | | Per-tenant fairness / avoiding noisy neighbors (usually not supported) | Fairness keys and weights — Public Preview, and a paid feature in Temporal Cloud. See `references/core/priority-fairness.md` | -| Delayed job (`countdown`, `enqueue_in`, `perform_in`) | A start delay on the Standalone Activity itself — no Workflow needed. Any duration, at any scale | +| Delayed job (`countdown`, `enqueue_in`, `perform_in`) | A start delay on the Standalone Activity itself — no Workflow needed. Requires Server 1.32.0+ or Temporal Cloud | | Dashboard (Flower, Sidekiq Web, Bull Board) | Temporal Web UI, `temporal activity list` (with Search Attribute support) / `describe`, and the list/count client APIs | | Job metrics | Standard Activity metrics: scheduled, started, completed, failed, timed out, canceled | | Manual/external job completion | Manual completion by Activity ID or task token | @@ -64,7 +64,7 @@ Not every "job queue" request is a single job. Route these away from Standalone | A job that waits for human approval or an external event | A **Workflow** with a Signal or Update handler. | | Long-lived per-entity state (a per-user or per-order actor) | The **entity Workflow** pattern — see `references/core/patterns.md`. | -A one-shot delayed job ("run this in 10 minutes") uses a start delay on the Standalone Activity itself — `start_delay` on the start request, or `--start-delay` on `temporal activity start`. Temporal accepts any duration at any scale, where job frameworks like Celery limit both. +A one-shot delayed job ("run this in 10 minutes") uses a start delay on the Standalone Activity itself — `start_delay` on the start request, or `--start-delay` on `temporal activity start`. This needs Temporal Server 1.32.0 or higher (or Temporal Cloud); check the version in play before recommending it. Temporal accepts long delays at scale, where job frameworks like Celery limit both. Rule of thumb: **one unit of work → Standalone Activity, delayed or not; more than one step, or waiting on an external event → Workflow.** @@ -91,7 +91,7 @@ Framework-specific notes worth stating when they come up: Anti-patterns to avoid when building a job queue on Temporal: -1. **A Workflow per job that runs exactly one Activity.** It costs an extra billable Action and extra Worker round-trips for no orchestration benefit. Prefer a Standalone Activity, including for delayed jobs, which take a start delay directly. Recurring jobs on a Schedule are the exception, since Schedules start Workflows. +1. **A Workflow per job that runs exactly one Activity.** It costs an extra billable Action and extra Worker round-trips for no orchestration benefit. Prefer a Standalone Activity, including for delayed jobs, which take a start delay directly on a recent enough Server. Recurring jobs on a Schedule are the exception, since Schedules start Workflows. 2. **A long-lived "queue manager" Workflow** that accepts jobs by Signal and dispatches them. It reinvents a queue the Server already provides, grows unbounded Event History, forces continue-as-new, and reintroduces head-of-line blocking. 3. **An Activity that polls Redis/SQS/a database table for work** and then dispatches it. Once on Temporal, the producer should enqueue Standalone Activities directly. (Polling an external system you do not control is a different, legitimate pattern — see `references/core/patterns.md`.) 4. **Hand-rolled retry loops inside the Activity.** Configure a Retry Policy instead; a `for attempt in range(3)` inside an Activity hides failures from visibility and metrics. diff --git a/references/ruby/ruby.md b/references/ruby/ruby.md index cf771eae..f3bff66e 100644 --- a/references/ruby/ruby.md +++ b/references/ruby/ruby.md @@ -149,3 +149,4 @@ See `references/ruby/testing.md` for info on writing tests. - **`references/ruby/observability.md`** - Logging, metrics, tracing, Search Attributes - **`references/ruby/gotchas.md`** - Ruby-specific mistakes and anti-patterns - **`references/ruby/advanced-features.md`** - Schedules, worker tuning, and more +- **`references/ruby/standalone-activities.md`** - Standalone Activities: run an Activity directly from a Client without a Workflow (Public Preview). Concept overview at `references/core/standalone-activities.md`. diff --git a/references/ruby/standalone-activities.md b/references/ruby/standalone-activities.md index bac2bd3c..fc8da226 100644 --- a/references/ruby/standalone-activities.md +++ b/references/ruby/standalone-activities.md @@ -30,7 +30,7 @@ end ```ruby require 'temporalio/client' -require 'temporalio/envconfig' +require 'temporalio/env_config' require 'temporalio/worker' args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options @@ -62,7 +62,7 @@ The Standalone Activity operations are methods on a connected `Temporalio::Clien ```ruby require 'temporalio/client' -require 'temporalio/envconfig' +require 'temporalio/env_config' args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options args[0] ||= 'localhost:7233' From e0037ea44b3dd02b20ed13a5203c9cdbfaacefa4 Mon Sep 17 00:00:00 2001 From: starfleeth <128422269+starfleeth@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:46:54 -0700 Subject: [PATCH 7/8] Drop the unverified start-delay version floor The 1.32.0 floor was inferred from an unreleased dev tag, and this PR is held until Standalone Activities go GA, by which point start delay is in the current release. Says to update to the latest Server instead of naming a version that cannot be verified while the PR sits in draft. A floor can go back once GA pins one. Co-Authored-By: Claude Opus 5 --- references/core/job-queue.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/references/core/job-queue.md b/references/core/job-queue.md index 36bf992e..7dec4e08 100644 --- a/references/core/job-queue.md +++ b/references/core/job-queue.md @@ -44,7 +44,7 @@ See `references/core/standalone-activities.md` for the rest of the feature list | Cancel a job | `cancel` (cooperative, surfaced on the next heartbeat) or `terminate` (forceful) | | Priority queues | Priority keys — free, Public Preview. See `references/core/priority-fairness.md` | | Per-tenant fairness / avoiding noisy neighbors (usually not supported) | Fairness keys and weights — Public Preview, and a paid feature in Temporal Cloud. See `references/core/priority-fairness.md` | -| Delayed job (`countdown`, `enqueue_in`, `perform_in`) | A start delay on the Standalone Activity itself — no Workflow needed. Requires Server 1.32.0+ or Temporal Cloud | +| Delayed job (`countdown`, `enqueue_in`, `perform_in`) | A start delay on the Standalone Activity itself — no Workflow needed | | Dashboard (Flower, Sidekiq Web, Bull Board) | Temporal Web UI, `temporal activity list` (with Search Attribute support) / `describe`, and the list/count client APIs | | Job metrics | Standard Activity metrics: scheduled, started, completed, failed, timed out, canceled | | Manual/external job completion | Manual completion by Activity ID or task token | @@ -64,7 +64,7 @@ Not every "job queue" request is a single job. Route these away from Standalone | A job that waits for human approval or an external event | A **Workflow** with a Signal or Update handler. | | Long-lived per-entity state (a per-user or per-order actor) | The **entity Workflow** pattern — see `references/core/patterns.md`. | -A one-shot delayed job ("run this in 10 minutes") uses a start delay on the Standalone Activity itself — `start_delay` on the start request, or `--start-delay` on `temporal activity start`. This needs Temporal Server 1.32.0 or higher (or Temporal Cloud); check the version in play before recommending it. Temporal accepts long delays at scale, where job frameworks like Celery limit both. +A one-shot delayed job ("run this in 10 minutes") uses a start delay on the Standalone Activity itself — `start_delay` on the start request, or `--start-delay` on `temporal activity start`. It requires a recent Temporal Server; if the option is missing, update to the latest version. Temporal accepts long delays at scale, where job frameworks like Celery limit both. Rule of thumb: **one unit of work → Standalone Activity, delayed or not; more than one step, or waiting on an external event → Workflow.** @@ -91,7 +91,7 @@ Framework-specific notes worth stating when they come up: Anti-patterns to avoid when building a job queue on Temporal: -1. **A Workflow per job that runs exactly one Activity.** It costs an extra billable Action and extra Worker round-trips for no orchestration benefit. Prefer a Standalone Activity, including for delayed jobs, which take a start delay directly on a recent enough Server. Recurring jobs on a Schedule are the exception, since Schedules start Workflows. +1. **A Workflow per job that runs exactly one Activity.** It costs an extra billable Action and extra Worker round-trips for no orchestration benefit. Prefer a Standalone Activity, including for delayed jobs, which take a start delay directly. Recurring jobs on a Schedule are the exception, since Schedules start Workflows. 2. **A long-lived "queue manager" Workflow** that accepts jobs by Signal and dispatches them. It reinvents a queue the Server already provides, grows unbounded Event History, forces continue-as-new, and reintroduces head-of-line blocking. 3. **An Activity that polls Redis/SQS/a database table for work** and then dispatches it. Once on Temporal, the producer should enqueue Standalone Activities directly. (Polling an external system you do not control is a different, legitimate pattern — see `references/core/patterns.md`.) 4. **Hand-rolled retry loops inside the Activity.** Configure a Retry Policy instead; a `for attempt in range(3)` inside an Activity hides failures from visibility and metrics. From 7dd3c576479dbf6edbe1fc5c5d035f2da31d50c4 Mon Sep 17 00:00:00 2001 From: starfleeth <128422269+starfleeth@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:25:34 -0500 Subject: [PATCH 8/8] Drop roadmap language from job-queue.md The skill states what is true now, not what is planned. Removes "Scheduled Standalone Activities are coming in a future release", "that is planned for a future release", and the start-delay caveat pointing at an unreleased Server version. Co-Authored-By: Claude Opus 5 --- references/core/job-queue.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/references/core/job-queue.md b/references/core/job-queue.md index 7dec4e08..60307dd1 100644 --- a/references/core/job-queue.md +++ b/references/core/job-queue.md @@ -60,11 +60,11 @@ Not every "job queue" request is a single job. Route these away from Standalone | Chained jobs, DAGs, Celery canvas / chords, "when job A finishes run B and C" | A **Workflow**. That is orchestration, which is what Workflows are for. | | Fan-out with a join, or a batch with a completion callback | A **Workflow** that starts the Activities in parallel and awaits them. | | Compensation / rollback when a later step fails | A **Workflow** using the saga pattern — see `references/core/patterns.md`. | -| Recurring or periodic jobs (Celery beat, `sidekiq-cron`, a crontab) | A **Temporal Schedule**, which starts a thin Workflow that calls the one Activity. Scheduled Standalone Activities are coming in a future release. | +| Recurring or periodic jobs (Celery beat, `sidekiq-cron`, a crontab) | A **Temporal Schedule**, which starts a thin Workflow that calls the one Activity. | | A job that waits for human approval or an external event | A **Workflow** with a Signal or Update handler. | | Long-lived per-entity state (a per-user or per-order actor) | The **entity Workflow** pattern — see `references/core/patterns.md`. | -A one-shot delayed job ("run this in 10 minutes") uses a start delay on the Standalone Activity itself — `start_delay` on the start request, or `--start-delay` on `temporal activity start`. It requires a recent Temporal Server; if the option is missing, update to the latest version. Temporal accepts long delays at scale, where job frameworks like Celery limit both. +A one-shot delayed job ("run this in 10 minutes") uses a start delay on the Standalone Activity itself — `start_delay` on the start request, or `--start-delay` on `temporal activity start`. Temporal accepts long delays at scale, where job frameworks like Celery limit both. Rule of thumb: **one unit of work → Standalone Activity, delayed or not; more than one step, or waiting on an external event → Workflow.** @@ -106,7 +106,7 @@ A job queue built on Standalone Activities has three pieces, and they belong in - **The Worker** — registers the Activity and polls the Task Queue. It does not know or care whether the Activity will be invoked standalone or from a Workflow. - **The producer** — application code, an HTTP handler, or a CLI entry point that calls the Client. -A Workflow that needs a job to outlive it can start a Standalone Activity from inside a regular in-Workflow Activity, using the SDK Client there. Workflow code cannot start one directly today; that is planned for a future release. +A Workflow that needs a job to outlive it can start a Standalone Activity from inside a regular in-Workflow Activity, using the SDK Client there. Workflow code cannot start one directly. ## SDK guides and runnable samples