A minimal Python sample: a real LLM generates a greeting through a REST call in a LangGraph node executed as a Temporal Activity. The graph runs inside a pinned Temporal Workflow. There is no provider SDK or observability integration.
Start here: local quickstart · Cloud and configuration · EKS deployment · CI/CD · troubleshooting · extending and upgrading.
The sample includes real-plugin integration tests, history replay, immutable container packaging, Temporal Worker Controller manifests, and reusable CI/CD scripts. A fresh-clone test run passed all 34 tests, and a real OpenAI greeting completed through a local Temporal server. Jenkins is an illustrative runner adapter. See verification and known limits for exactly what has been demonstrated and what still needs customer infrastructure.
Prerequisites: Python 3.12 (selected by .python-version), uv, the Temporal CLI, and an OpenAI API key with access to the configured model. The Worker signal handling targets macOS/Linux. The locked environment was tested with Python 3.12.14, temporalio 1.33.0, LangGraph 1.2.11, and Temporal CLI 1.5.1 / Server 1.29.1.
From this directory, install the locked packages and create your local configuration:
uv sync --frozen
cp .env.example .envEdit .env to set OPENAI_API_KEY. The example selects gpt-4.1-mini-2025-04-14 and BUILD_ID=hello-v5. If you already created .env for an earlier version, update its build ID rather than reusing an old ID for the changed code. Keep the code, dependency lock, model selection, and prompt unchanged for a given build ID. The application reads environment variables; uv loads the .env file. .env is ignored by Git.
TEMPORAL_TASK_QUEUE injects the worker's task queue locally and on EKS; local runs default to hello-agent. The commands below use that example name. If you configure another queue, use the same name in each CLI --task-queue argument. Keep it stable across versions of the same worker service.
In terminal 1, start a persistent local Temporal dev server:
mkdir -p .temporal
temporal server start-dev --ip 127.0.0.1 --db-filename .temporal/temporal.dbIn terminal 2, start the versioned Worker:
uv run --frozen --env-file .env python app.pyIn terminal 3, promote the running build, then submit a greeting:
temporal worker deployment set-current-version \
--address localhost:7233 --namespace default --disable-config-file --disable-config-env \
--deployment-name hello-agent --build-id hello-v5 --yes
temporal workflow execute \
--address localhost:7233 --namespace default --disable-config-file --disable-config-env \
--type HelloWorkflow --task-queue hello-agent \
--workflow-id hello/demo-001 --input '"Pepsi"' \
--id-conflict-policy Fail --id-reuse-policy RejectDuplicate \
--fail-existingPromote only after the Worker is running. If the CLI reports that pollers are not yet registered, retry the promotion after registration; do not bypass the poller check. If you change the address, namespace, or build ID, use the matching values in both the Worker configuration and CLI command.
execute submits the Workflow and waits for its result. --input '"Pepsi"' supplies a JSON string, matching HelloWorkflow.run(name: str). The Worker needs the LLM key, model, and build ID; the CLI only needs Temporal connection settings. The CLI commands here explicitly use the local address and namespace; they do not load the Worker's .env file.
To submit without waiting, use start with a new Workflow ID:
temporal workflow start \
--address localhost:7233 --namespace default --disable-config-file --disable-config-env \
--type HelloWorkflow --task-queue hello-agent \
--workflow-id hello/demo-002 --input '"Pepsi"' \
--id-conflict-policy Fail --id-reuse-policy RejectDuplicate \
--fail-existing
temporal workflow result \
--address localhost:7233 --namespace default --disable-config-file --disable-config-env \
--workflow-id hello/demo-002Set the ID policies on each submission: Fail prevents a conflicting running execution, and RejectDuplicate prevents reuse of a closed execution's ID while retained. These are start-request options, not Worker registration settings. --fail-existing makes the CLI report duplicate submission as an error instead of returning the existing execution. Use a new Workflow ID for each intentionally new greeting.
Inspect execution history in the local Temporal UI. Ctrl-C stops the Worker gracefully. Stopping execute or result does not cancel the Workflow; inspect it by ID rather than starting another request accidentally.
Run the tests without an API key:
bash ci/test.shTests start and stop an isolated Temporal dev server on an available local port. They do not use your persistent demo server or send requests to OpenAI. Expected: all tests pass and .ci-artifacts/junit.xml is created. No .env or API key is needed for this command.
For the real greeting, expect the Worker to print Worker started: hello-agent / hello-v5 / hello-agent, the promotion command to succeed, and workflow execute to report a completed Workflow with a short greeting string. The exact wording varies. Stop the Worker and dev server with Ctrl-C; the dev database remains in .temporal/. Every intentionally new greeting needs a new Workflow ID.
Temporal CLI → Temporal Workflow → LangGraph greeting node → LLM
(Temporal Activity)
The Workflow orchestrates the graph. The Activity calls the model. Temporal records the Activity result and uses that recorded result during Workflow replay. If a call completes at the provider but its result is not recorded by Temporal, retrying the Activity can call the provider again and incur another charge.
Call OpenAI's POST https://api.openai.com/v1/chat/completions directly with httpx.AsyncClient. Load OPENAI_API_KEY and LLM_MODEL from the Worker environment. Use a model that supports that endpoint. There will be no OpenAI SDK, LangChain model wrapper, or other provider SDK in the application. The HTTP timeout, response parsing, and classification of permanent versus transient failures remain explicit. REST API reference.
The official temporalio[langgraph] integration provides temporalio.contrib.langgraph.LangGraphPlugin. Our StateGraph(str) contains one node marked execute_in: activity; the plugin registers it as a Temporal Activity without a manual @activity.defn. The integration is Public Preview; the upstream repository also labels it experimental. SDK 1.33.0 is pinned in pyproject.toml and all resolved versions are recorded in uv.lock. Integration guide, plugin source documentation.
The first section below describes implemented practices. Later sections distinguish features needed only for future samples and demonstrations that remain pending.
- Deterministic orchestration. Keep network calls, model initialization, environment reads, and other external I/O outside Workflow execution. Keep Workflow definitions separate from node implementations and use the SDK sandbox with appropriate import passthrough.
- Small Activity boundaries. Run the greeting call as an Activity through the plugin. A future tool that writes to an external system gets its own Activity instead of sharing a retry boundary with the model call.
- Explicit timeout and retry budgets. HTTP has a 20-second total deadline; the Activity has a 30-second attempt timeout, a two-minute queue/retry budget, and at most three attempts with Temporal's exponential backoff. Transport failures, HTTP 408/409/429, and 5xx responses are retryable. Other non-200 responses, invalid input/configuration, and malformed or incomplete greetings fail without retry. There are no provider SDK or LangGraph retry loops. No overall Workflow timeout arbitrarily truncates durable execution.
- Async I/O. Use
httpx.AsyncClientfor direct REST requests inside the async node. Do not place blocking HTTP calls or sleeps on the Worker event loop. - Serializable, bounded input and output. The graph exchanges strings. Names are limited to 100 characters after trimming; requests cap output at 128 tokens and returned greetings at 4,096 characters. The Worker reads credentials/model settings once at startup and injects them into
HelloNodes; neither credentials nor client objects are passed as Workflow arguments or graph state. Provider error bodies are not copied into application errors. Workflow inputs and model outputs are persisted in history. - Stable request identity. The documented CLI commands supply an explicit Workflow ID, conflict/reuse policies, and
--fail-existing. Repeated submission with those options does not create another greeting run. The Worker cannot enforce callers' start-request policies. Temporal ID deduplication is bounded by execution/retention semantics, not a permanent business ledger. - Safe Worker shutdown. SIGINT/SIGTERM trigger Worker shutdown with a 35-second grace period. Activity concurrency is limited to 10. The shutdown entry point is tested; recovery after an abrupt crash is a separate, pending demonstration.
- Locked dependencies.
uv.lockrecords the dependency versions. Treat application code, dependencies, and model/prompt configuration as one immutable deployment build.
Use the current Worker Deployment APIs with VersioningBehavior.PINNED, an explicit deployment name, and a build ID that changes when the release changes. Keep the Workflow and its generated graph Activities in the same deployment/task queue.
With version A running, start version B separately and promote B for new executions. Existing A executions continue on A. Keep A workers available until their pinned executions finish; retain the old artifact for any later replay or recovery needs. Editing A's files and restarting them under A's existing build ID is not safe versioning.
Pinned execution does not automatically migrate an existing run to new code. If we later need that behavior, design a replay-compatible change or explicit migration. AUTO_UPGRADE requires compatibility discipline, including workflow.patched() where the command sequence changes. A model name alone also does not guarantee immutable provider behavior.
Worker Versioning concepts, deployment guide.
- External writes: use a stable logical-operation idempotency key enforced by the destination. Include the operation occurrence when a tool can run repeatedly. Workflow ID deduplication does not deduplicate external writes, and retries do not guarantee exactly-once effects.
- Long Activities: add actual periodic heartbeats, a heartbeat timeout, and cancellation handling together. A heartbeat timeout without heartbeats makes healthy work time out. A short, tightly bounded greeting call does not need a heartbeat loop just to satisfy a checklist.
- Agent loops: set explicit turn, tool-call, and output budgets with a controlled termination path. Measure payload/history growth; introduce Continue-As-New at a deliberate state boundary if runs become long-lived.
- Human approval: use durable Workflow waiting and an acknowledged, validated decision mechanism. Ensure any overall timeout allows the approval window and cleanup. Child workflows need deliberate lifecycle policies if introduced.
- Shared memory: keep per-run data in Workflow/graph state. The plugin does not transport a LangGraph Store into Activity nodes. A separate persistent LangGraph checkpointer is unnecessary for Temporal durability; the plugin documents
InMemorySaverwhen an interrupt requires a checkpointer. - Deployment durability: use a persistent local dev-server database for restart demonstrations. Production durability requires a suitably operated Temporal Service or Temporal Cloud; Python Worker code cannot provide that infrastructure guarantee by itself.
Activity idempotency and execution semantics, Python error handling.
Read the verification record before presenting this as a production reference. Local tests cover the real plugin, replay, CLI submission and duplicates, selected retry behavior, idle shutdown, HTTP helpers and release rendering/readiness. They do not establish live EKS routing, crash recovery or in-flight draining. The LangGraph integration is Public Preview and its version is pinned.
Keep each sample's nodes, graph, and Workflow in separate modules. app.py connects to Temporal and runs the Worker. Workflow submission is handled by the Temporal CLI; there is no application starter module.
app.py # Temporal connection and Worker lifecycle
samples/hello/
nodes.py # LLM REST call, response validation, failure classification
graph.py # Graph edges and Activity timeout/retry metadata
workflow.py # Pinned Workflow invoking the graph
shared/http.py # Shared async HTTP client: get/post/put
ci/ # Test, publish, deploy, smoke, manifest renderer and guide
Jenkinsfile # Illustrative CI runner adapter
deploy/eks/ # Controller values, Connection and WorkerDeployment templates
tests/ # Integration/unit tests and saved history fixture
Dockerfile, .dockerignore # Minimal runtime image and restricted build context
docs/ # Configuration, troubleshooting, extension and verification
.env.example # Local configuration template; no credentials
.python-version # Supported Python minor version
pyproject.toml, uv.lock # Declared and resolved dependencies
The Worker constructs these objects once:
HttpClient → HelloNodes(http, api_key=..., model=...) → HelloGraph(nodes).build()
↓
LangGraphPlugin("hello")
↑
HelloWorkflow.run()
HttpClient exposes separate async get(), post(), and put() methods and owns one reusable httpx.AsyncClient connection pool. Its async context manager closes the pool after Worker shutdown completes. HelloNodes holds the HTTP client and fixed provider configuration; each greeting stays in method-local variables. HelloGraph.build() creates a fresh graph for plugin registration, including the Activity execution policy. HelloWorkflow remains the deterministic, pinned Temporal entry point.
The node and HTTP client are shared across concurrent Activities, so per-run mutable state must stay in method-local variables or Workflow/graph state. Credentials and client objects are not serialized into that state. app.py contains small functions for plugin registration, Worker creation/lifecycle, and startup configuration. It still uses a Temporal Client to connect the Worker to the service. Add other HTTP methods only when a sample needs them.
HTTP transport runs inside an Activity node; it is not a separate Activity and has no independent retry loop. Nodes interpret provider-specific responses and classify permanent failures. Each sample has an explicitly named Workflow; no generic workflow framework is needed.
The Worker registers this sample explicitly. Add another sample's graph and named Workflow to that registration when needed, then select its Workflow type and input in the CLI.
Research checked on 17 September 2026 against official Temporal documentation. Implementation tests passed using Temporal CLI 1.5.1 (bundled Server 1.29.1), SDK 1.33.0, LangGraph 1.2.11, Python 3.12.14, and uv 0.12.15. LangGraph pulls LangSmith in as a transitive package, but this project configures no tracing plugin, exporter, or observability service.
The supplied Pepsi review is useful test-planning input, not proof of current defects. Its H1 claim about mandatory Client registration differs from the current official LangGraph example. This implementation's real-plugin tests pass with a plain Client and Worker-side registration; that result applies to this pinned string-state example, not every possible converter or future plugin release.