Hook event catalogue
Each event below is tagged live or planned.
Live events fire from runtime call sites today — your callbacks will be invoked.
Planned events have typed contexts and registration works, but the runtime doesn't fire them yet. Subscribing is harmless — the callback simply never runs. Call sites are added on demand; open an issue if you need one wired.
Action protocols
Hooks can return objects that change runtime behaviour. The first matching action wins — non-matching returns are silently ignored, so observer-only hooks coexist freely with action hooks.
from flowly.agent.hooks import BlockAction, RewriteAction, SkipAction
# pre_tool_call → abort dispatch with a message
return BlockAction(message="rate-limited")
# pre_gateway_dispatch → drop the message entirely (no reply)
return SkipAction(reason="spam filter")
# pre_gateway_dispatch → replace the inbound text and continue
return RewriteAction(text="redacted: <user query>")
# transform_tool_result / transform_terminal_output
# → return a bare string to replace the output
return "results redacted for privacy"
# pre_llm_call → return context to inject into the user message
return {"context": "Previous insight: user prefers dark mode."}
# or simply:
return "Previous insight: user prefers dark mode."Tool lifecycle
pre_tool_call(ToolHookContext)liveFires before any tool dispatch. Plugin can short-circuit by returning BlockAction.
BlockAction | Nonepost_tool_call(ToolHookContext)liveFires after tool dispatch with result, duration, success populated. Observation only.
Nonetransform_tool_result(ToolHookContext)liveFires after post_tool_call. Return a string to replace the result the agent sees.
str | Nonetransform_terminal_output(ToolHookContext)plannedDefined for exec/shell tools to rewrite stdout/stderr before the agent sees it. Call site not yet wired in core.
str | NoneLLM lifecycle
pre_llm_call(LLMHookContext)liveFires before each LLM API call. Returned strings/dicts get appended to the user message — never the system prompt — preserving cache.
str | {"context": str} | Nonepost_llm_call(LLMHookContext)plannedDefined for plugins that need to react to each response (custom analytics). Note: token/usage tracking is already handled at the provider layer — surfaced via LLMResponse.usage.
Nonepre_api_request(LLMHookContext)plannedDefined: lower level, fires before raw HTTP request to provider. Useful for external observability tools.
Nonepost_api_request(LLMHookContext)plannedDefined: lower level, fires after raw HTTP response. Useful for external observability tools.
NoneSession lifecycle
on_session_start(SessionHookContext)liveFires the first time a session_key is seen by the agent. Use for warming caches or initialization.
Noneon_session_end(SessionHookContext)liveFires after every turn (each user message + agent reply pair). Used by disk-cleanup for per-turn cleanup.
Noneon_session_finalize(SessionHookContext)plannedDefined for end-of-process flush (export to S3/Notion, archive). Call site not yet wired.
Noneon_session_reset(SessionHookContext)plannedDefined to fire when a user clears a session via /clear or /new. Call site not yet wired.
NoneSubagents
subagent_stop(SubagentStopContext)plannedDefined to fire when a delegated subagent task terminates. Call site not yet wired.
NoneGateway dispatch
pre_gateway_dispatch(GatewayDispatchContext)liveFires for every inbound message (telegram/web/desktop/iOS/cli) before session work begins. Return SkipAction to drop the message or RewriteAction to replace its content. Used by the pii-redactor example.
SkipAction | RewriteAction | NoneContext shapes
Imported from flowly.agent.hooks. Every context inherits common session_id and task_id fields.
@dataclass
class HookContext:
session_id: str = ""
task_id: str = ""
@dataclass
class ToolHookContext(HookContext):
tool_name: str = ""
params: dict = field(default_factory=dict)
tool_call_id: str = ""
# Populated for post_tool_call / transform_*:
result: str | None = None
duration_ms: float = 0.0
success: bool | None = None
@dataclass
class LLMHookContext(HookContext):
model: str = ""
messages: list[dict] = field(default_factory=list)
tools: list[dict] = field(default_factory=list)
system: str = ""
user_message: str = ""
# Populated for post_llm_call:
response: Any = None
assistant_message: str = ""
usage: dict = field(default_factory=dict)
truncated: bool = False
interrupted: bool = False
@dataclass
class SessionHookContext(HookContext):
model: str = ""
platform: str = "" # "telegram" | "web" | "desktop" | ...
completed: bool = True
interrupted: bool = False
@dataclass
class SubagentStopContext(HookContext):
subagent_id: str = ""
reason: str = ""
@dataclass
class GatewayDispatchContext(HookContext):
event: Any = None # InboundMessage instance
gateway: Any = None
session_store: Any = None