@@ -12,10 +12,13 @@ that runs for the duration of a state and is cancelled when the state is exited.
1212Invoke handlers run ** outside** the main state machine processing loop:
1313
1414- ** Sync engine** : each invoke handler runs in a ** daemon thread** .
15- - ** Async engine** : each invoke handler runs in a ** thread executor**
16- (` loop.run_in_executor ` ), wrapped in an ` asyncio.Task ` . The executor is used because
17- invoke handlers are expected to perform blocking I/O (network calls, file access,
18- subprocess communication) that would freeze the event loop if run directly.
15+ - ** Async engine** :
16+ - ** Sync handlers** run in a ** thread executor** (` loop.run_in_executor ` ), wrapped
17+ in an ` asyncio.Task ` . The executor is used because blocking I/O (network calls,
18+ file access, subprocess communication) would freeze the event loop if run directly.
19+ - ** Coroutine functions** and ` IInvoke ` handlers with ` async def run() ` are
20+ ** awaited directly** on the event loop, with no executor overhead. This is the
21+ natural choice for non-blocking async I/O (e.g., ` aiohttp ` , async DB drivers).
1922
2023When a handler completes, a ` done.invoke.<state>.<id> ` event is automatically sent back
2124to the machine. If the handler raises an exception, an ` error.execution ` event is sent
@@ -270,6 +273,86 @@ Events from cancelled invocations are silently ignored.
270273
271274```
272275
276+ ## Coroutine functions
277+
278+ Coroutine functions (` async def ` ) can be used as invoke targets. On the async engine,
279+ they are awaited directly on the event loop instead of running in a thread executor.
280+ This is ideal for non-blocking async I/O:
281+
282+ ``` py
283+ >> > import asyncio
284+
285+ >> > async def async_fetch ():
286+ ... await asyncio.sleep(0.01 ) # simulates async I/O
287+ ... return {" status" : " ok" }
288+
289+ >> > class AsyncLoader (StateChart ):
290+ ... loading = State(initial = True , invoke = async_fetch)
291+ ... ready = State(final = True )
292+ ... done_invoke_loading = loading.to(ready)
293+ ...
294+ ... def on_enter_ready (self , data = None , ** kwargs ):
295+ ... self .result = data
296+
297+ >> > async def main ():
298+ ... sm = AsyncLoader()
299+ ... await sm.activate_initial_state()
300+ ... await asyncio.sleep(0.1 )
301+ ... await sm._processing_loop()
302+ ... return sm
303+
304+ >> > sm = asyncio.run(main())
305+
306+ >> > " ready" in sm.configuration_values
307+ True
308+ >> > sm.result
309+ {' status' : ' ok' }
310+
311+ ```
312+
313+ The ` IInvoke ` protocol also supports ` async def run() ` . Since ` IInvoke ` handlers
314+ are wrapped internally, you need at least one async callback in the machine to
315+ trigger the async engine (e.g., an ` async def ` action or listener):
316+
317+ ``` py
318+ >> > class AsyncFetcher :
319+ ... async def run (self , ctx : InvokeContext):
320+ ... await asyncio.sleep(0.01 )
321+ ... return " async_fetched"
322+
323+ >> > class AsyncFetcherMachine (StateChart ):
324+ ... loading = State(initial = True , invoke = AsyncFetcher)
325+ ... ready = State(final = True )
326+ ... done_invoke_loading = loading.to(ready)
327+ ...
328+ ... async def on_enter_ready (self , data = None , ** kwargs ):
329+ ... self .result = data
330+
331+ >> > async def run_fetcher ():
332+ ... sm = AsyncFetcherMachine()
333+ ... await sm.activate_initial_state()
334+ ... await asyncio.sleep(0.1 )
335+ ... await sm._processing_loop()
336+ ... return sm
337+
338+ >> > sm = asyncio.run(run_fetcher())
339+
340+ >> > " ready" in sm.configuration_values
341+ True
342+ >> > sm.result
343+ ' async_fetched'
344+
345+ ```
346+
347+ Cancellation of coroutine handlers works through ` asyncio.Task.cancel() ` , which
348+ raises ` CancelledError ` at the next ` await ` point, giving proper async cancellation
349+ semantics without cooperative polling.
350+
351+ ``` {note}
352+ Coroutine functions automatically select the async engine. Using an `IInvoke` with
353+ `async def run()` on the sync engine raises `InvalidDefinition`.
354+ ```
355+
273356## Event data propagation
274357
275358When a state with invoke handlers is entered via an event, the keyword arguments from
0 commit comments