Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Some examples require extra dependencies. See each sample's directory for specif
* [hello_search_attributes](hello/hello_search_attributes.py) - Start workflow with search attributes then change
while running.
* [hello_signal](hello/hello_signal.py) - Send signals to a workflow.
* [hello_sync_client](hello/hello_sync_client.py) - Call the async Temporal client from synchronous Python code.
* [hello update](hello/hello_update.py) - Send a request to and a response from a client to a workflow execution.
<!-- Keep this list in alphabetical order -->
* [activity_worker](activity_worker) - Use Python activities from a workflow in another language.
Expand Down
14 changes: 14 additions & 0 deletions hello/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,22 @@ Replace `hello/hello_activity.py` in the command with any other example filename
* [hello_search_attributes](hello_search_attributes.py) - Start workflow with search attributes then change while
running.
* [hello_signal](hello_signal.py) - Send signals to a workflow.
* [hello_sync_client](hello_sync_client.py) - Call the async Temporal client from synchronous Python code.
* [hello_update](hello_update.py) - **Send a request to and a response from a client to a workflow execution.**

Note: To enable the workflow update, set the `frontend.enableUpdateWorkflowExecution` dynamic config value to true.

temporal server start-dev --dynamic-config-value frontend.enableUpdateWorkflowExecution=true

## Calling the client from synchronous code

Run `uv run hello/hello_sync_client.py` to call `greet("World")` from a normal
synchronous function and print `Result: Hello, World!`. The function uses
`asyncio.run()` to connect a client, execute a workflow, and wait for its result.
The sample also starts and stops a worker so it can run on its own.

Use this pattern from a thread that has no running event loop. If your application
is already asynchronous, await the SDK methods directly. Each call creates a new
event loop, so create and use the client inside that call rather than reusing a
client from an earlier call. For applications making many calls, prefer a
long-lived async client and a separately running worker.
50 changes: 50 additions & 0 deletions hello/hello_sync_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import asyncio
import uuid

from temporalio import workflow
from temporalio.client import Client
from temporalio.envconfig import ClientConfig
from temporalio.worker import Worker


@workflow.defn
class GreetingWorkflow:
@workflow.run
async def run(self, name: str) -> str:
return f"Hello, {name}!"


async def _execute_greeting(name: str) -> str:
# Create and use the client on the same event loop. Do not cache this client
# and reuse it across calls to asyncio.run(), which creates a new loop.
config = ClientConfig.load_client_connect_config()
config.setdefault("target_host", "localhost:7233")
client = await Client.connect(**config)
task_queue = f"hello-sync-client-{uuid.uuid4()}"

# Keep the sample self-contained by running a worker here. In a production
# application, workers usually run in a separate process on a shared queue.
async with Worker(client, task_queue=task_queue, workflows=[GreetingWorkflow]):
return await client.execute_workflow(
GreetingWorkflow.run,
name,
id=f"hello-sync-client-{uuid.uuid4()}",
task_queue=task_queue,
)


def greet(name: str) -> str:
"""Execute a workflow from synchronous code and wait for its result."""
# This is the boundary between the synchronous application and the async SDK.
# Call it from a thread without a running event loop. In an async application,
# await the async SDK directly instead.
return asyncio.run(_execute_greeting(name))


def main() -> None:
# Existing synchronous application code does not need async/await.
print(f"Result: {greet('World')}")


if __name__ == "__main__":
main()
29 changes: 29 additions & 0 deletions tests/hello/hello_sync_client_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import asyncio

import pytest
from temporalio.client import Client
from temporalio.envconfig import ClientConfig

from hello.hello_sync_client import greet


async def test_greet_from_sync_code(client: Client, monkeypatch: pytest.MonkeyPatch):
# Connect the sample's own client to the test environment (local or
# time-skipping), rather than sharing a client across event loops.
monkeypatch.setattr(
ClientConfig,
"load_client_connect_config",
lambda: {
"target_host": client.service_client.config.target_host,
"namespace": client.namespace,
},
)

def call_from_sync_code():
# Repeated calls create fresh loops and must both complete successfully.
assert greet("World") == "Hello, World!"
assert greet("Temporal") == "Hello, Temporal!"

# Keep the test environment's event loop available while the synchronous
# caller blocks waiting for the workflow result on its own thread.
await asyncio.to_thread(call_from_sync_code)
Loading