Hooks¶
Junjo hooks are optional, in-process Python callbacks for observing workflow lifecycle events. Hooks do not control workflow execution, and they are separate from OpenTelemetry, which stays active whether or not you register hooks.
Simple completion logging¶
from junjo import BaseState, BaseStore, Graph, Hooks, Node, Workflow
from junjo.hooks import WorkflowCompletedEvent
class MyState(BaseState):
message: str = "hello"
class MyStore(BaseStore[MyState]):
pass
class MyNode(Node[MyStore]):
async def service(self, store: MyStore) -> None:
return None
def create_graph() -> Graph:
node = MyNode()
return Graph(source=node, sinks=[node], edges=[])
hooks = Hooks()
def log_completed(event: WorkflowCompletedEvent[MyState]) -> None:
print(
event.hook_name,
{
"workflow_name": event.name,
"run_id": event.run_id,
"executable_definition_id": event.executable_definition_id,
"executable_runtime_id": event.executable_runtime_id,
"executable_structural_id": event.executable_structural_id,
"enclosing_graph_structural_id": event.enclosing_graph_structural_id,
"trace_id": event.trace_id,
"span_id": event.span_id,
"message": event.result.state.message,
},
)
hooks.on_workflow_completed(log_completed)
workflow = Workflow[MyState, MyStore](
name="My Workflow",
graph_factory=create_graph,
store_factory=lambda: MyStore(initial_state=MyState()),
hooks=hooks,
)
async def main() -> None:
await workflow.execute()
if __name__ == "__main__":
import asyncio
asyncio.run(main())
Additional hook fields¶
Different lifecycle events carry additional fields for the specific event type:
Workflow and subflow start events include
event.store_idandevent.graph_json.Workflow and subflow completion events include
event.store_idandevent.result. Useevent.result.stateto inspect the final state snapshot.Failure events include
event.error. Workflow and subflow failure events also includeevent.state.Cancellation events include
event.reason. Workflow and subflow cancellation events also includeevent.state.Node and run-concurrent lifecycle events include
event.store_idandevent.parent_executable_definition_id. They do not include final state snapshots.State-change events include
event.store_id,event.store_name,event.action_name,event.patch,event.state, andevent.parent_executable_definition_id.
Registering and unsubscribing¶
Every hooks.on_*() registration method returns an unsubscribe callback.
Call that returned function when you want to remove the callback from the
registry.
unsubscribe = hooks.on_workflow_completed(log_completed)
# Later, if the callback should no longer run:
unsubscribe()
Hook event state payloads are Python-side lifecycle data, not serialized
OpenTelemetry payloads. event.state is a detached copied state object for
in-process inspection inside your callback. That means telemetry-oriented state
serialization choices such as field exclusion or truncation do not
automatically change what appears on event.state.
For on_state_changed, the executable identity fields describe the active
workflow, subflow, node, or concurrent executable that actually triggered the
state update. The parent executable fields describe the containing execution
scope around that update.
Base example¶
The base example app registers every public hook with simple logging so you can see the complete lifecycle payloads that fire during a normal run:
workflow hooks
subflow hooks
node hooks
run-concurrent hooks
state-changed hooks