Problem / Motivation
Today, every node type (trigger, decision, ai-agent, visualize, and any future type) executes as an Activity inside the same execution-worker process, built from the same Docker image. This means:
- Every node type's dependencies must be installed into one shared image — no per-tool isolation.
- A future node type that needs heavyweight or exotic tooling (a coding-agent CLI such as Claude Code, Docker-in-Docker, GPU/CUDA, a specific Python toolchain,
ffmpeg, etc.) would bloat the shared image for every node type, or conflict with existing dependencies.
- All node types scale together — you can't run many replicas of a cheap node executor and one replica of an expensive one independently.
- No isolation/blast-radius boundary — a misbehaving dependency for one node type risks the whole worker process.
Goal
Allow specific node types to be executed by a dedicated worker process, running from its own Docker image with its own tools installed, while all other node types keep running on the existing general-purpose worker — with no changes to the workflow/interpreter code.
Non-goals
- Not proposing a container-per-activity-execution model (spinning up a fresh container per single node run). This is container-per-node-type-pool, i.e. a small number of long-lived specialized worker deployments, matching how Temporal is designed to scale.
- Not changing the graph interpreter (
runGraph) or the deterministic workflow contract at all — this is purely a worker-topology/deployment change.
Proposed Design: Temporal Task-Queue Routing
Temporal already supports this natively: any proxyActivities(...) call can target a specific task queue name. Workers subscribe to (poll) task queues; Temporal server only ever dispatches a task to a worker polling the matching queue. So:
- Introduce a
taskQueue field on the per-node-type activity profile, so specific node types can be pinned to a non-default queue.
- Stand up one additional worker deployment (own Dockerfile, own image, own installed tools) that polls that queue and registers only the executor(s) for that node type.
- Everything else keeps running exactly as today, on the existing default queue/worker/image.
No change needed to: runGraph, the graph JSON model, the UI, the DB schema, or the backend's submit() call — the workflow code doesn't know or care which container ran an activity.
Required Changes
1. packages/temporal — extend the activity-profile type
packages/temporal/src/workflow/activity-profiles.ts:
export type ActivityProfile = {
startToCloseTimeout: DurationString;
retry: { maximumAttempts: number };
taskQueue?: string; // NEW — defaults to the workflow's own task queue if omitted
};
Keep DEFAULT_NODE_ACTIVITY_PROFILE unchanged (no taskQueue = current behavior, zero risk to existing node types).
2. packages/temporal — wire it into proxyActivities
packages/temporal/src/workflow/run-workflow.ts, inside runner.executeNode, pass the resolved profile's taskQueue straight through to proxyActivities(...) (structurally compatible with Temporal's real ActivityOptions already). resolveFromValidatedProfiles/profile-validation.ts need to accept and pass through the optional taskQueue.
3. apps/execution-worker — a second, minimal entrypoint
A new activity-only worker entrypoint (no workflowsPath needed — Temporal supports activity-only workers) that registers only the specialized executor(s) and polls the new task queue name.
4. New Docker image for the specialized worker
- A new Dockerfile/build stage installing whatever the new node type needs, on top of the shared runtime layer.
- A new service in the production
docker-compose.yml alongside the existing worker service, connecting to the same Temporal server/namespace on a different task queue.
- A local dev script (own
.env if needed) to run it standalone.
5. Validation
profile-validation.ts (already validated at Worker.create time and inside the sandbox) should also guard against a node type routed to a taskQueue with no worker polling it — otherwise the activity silently sits pending forever, which is an easy and hard-to-diagnose misconfiguration.
6. Testing
- A test that a node profile's
taskQueue actually produces a ScheduleActivityTask command targeting that queue (inspect the recorded Event History, akin to the existing replay tests).
- A replay-determinism check confirming
taskQueue is pure routing metadata and doesn't leak into the sandbox's decision-making.
- A manual/integration check: run a diagram with the specialized node type, confirm in Temporal UI that its task lands on the new queue, not the default one.
Rollout / Compatibility
Fully additive and backward compatible: existing node types with no taskQueue in their profile behave exactly as today. Can be introduced node-type by node-type.
Open Questions
- Do specialized workers need their own
.env/credentials, separate from the main worker?
- Should long-running specialized activities (e.g. a CLI agent run) use Activity heartbeats rather than just a longer
startToCloseTimeout?
- Should a specialized activity's stdout/stderr integrate into the existing
emitEvent/execution-log pipeline as-is, or does it need a richer event shape (streaming partial output)?
- Should replica counts per worker type be independently configurable in the deploy compose file, given very different load/cost profiles per node type?
Summary
Add an optional taskQueue to the existing per-node-type ActivityProfile, thread it through to proxyActivities, and support standing up one additional worker process/Docker image per specialized node type that polls that queue — no changes to the graph interpreter, the workflow contract, or any other node type's behavior.
Problem / Motivation
Today, every node type (
trigger,decision,ai-agent,visualize, and any future type) executes as an Activity inside the sameexecution-workerprocess, built from the same Docker image. This means:ffmpeg, etc.) would bloat the shared image for every node type, or conflict with existing dependencies.Goal
Allow specific node types to be executed by a dedicated worker process, running from its own Docker image with its own tools installed, while all other node types keep running on the existing general-purpose worker — with no changes to the workflow/interpreter code.
Non-goals
runGraph) or the deterministic workflow contract at all — this is purely a worker-topology/deployment change.Proposed Design: Temporal Task-Queue Routing
Temporal already supports this natively: any
proxyActivities(...)call can target a specific task queue name. Workers subscribe to (poll) task queues; Temporal server only ever dispatches a task to a worker polling the matching queue. So:taskQueuefield on the per-node-type activity profile, so specific node types can be pinned to a non-default queue.No change needed to:
runGraph, the graph JSON model, the UI, the DB schema, or the backend'ssubmit()call — the workflow code doesn't know or care which container ran an activity.Required Changes
1.
packages/temporal— extend the activity-profile typepackages/temporal/src/workflow/activity-profiles.ts:Keep
DEFAULT_NODE_ACTIVITY_PROFILEunchanged (notaskQueue= current behavior, zero risk to existing node types).2.
packages/temporal— wire it intoproxyActivitiespackages/temporal/src/workflow/run-workflow.ts, insiderunner.executeNode, pass the resolved profile'staskQueuestraight through toproxyActivities(...)(structurally compatible with Temporal's realActivityOptionsalready).resolveFromValidatedProfiles/profile-validation.tsneed to accept and pass through the optionaltaskQueue.3.
apps/execution-worker— a second, minimal entrypointA new activity-only worker entrypoint (no
workflowsPathneeded — Temporal supports activity-only workers) that registers only the specialized executor(s) and polls the new task queue name.4. New Docker image for the specialized worker
docker-compose.ymlalongside the existingworkerservice, connecting to the same Temporal server/namespace on a different task queue..envif needed) to run it standalone.5. Validation
profile-validation.ts(already validated atWorker.createtime and inside the sandbox) should also guard against a node type routed to ataskQueuewith no worker polling it — otherwise the activity silently sits pending forever, which is an easy and hard-to-diagnose misconfiguration.6. Testing
taskQueueactually produces aScheduleActivityTaskcommand targeting that queue (inspect the recorded Event History, akin to the existing replay tests).taskQueueis pure routing metadata and doesn't leak into the sandbox's decision-making.Rollout / Compatibility
Fully additive and backward compatible: existing node types with no
taskQueuein their profile behave exactly as today. Can be introduced node-type by node-type.Open Questions
.env/credentials, separate from the main worker?startToCloseTimeout?emitEvent/execution-log pipeline as-is, or does it need a richer event shape (streaming partial output)?Summary
Add an optional
taskQueueto the existing per-node-typeActivityProfile, thread it through toproxyActivities, and support standing up one additional worker process/Docker image per specialized node type that polls that queue — no changes to the graph interpreter, the workflow contract, or any other node type's behavior.