Hooks Reference
This page summarizes hook types, signatures, and behaviors exposed by the modern interface. Prefer the decorators on ActingWebApp (app.property_hook(...) etc.).
Overview
Hook execution applies unified access control when enabled. For property hooks, returning None during write operations (PUT/POST/DELETE) rejects the change; returning None during GET hides the value from the caller and from the web UI.
Property Hooks
Decorator: app.property_hook(name: str = "*")
Signature: func(actor, operation: str, value: Any, path: List[str]) -> Optional[Any]
operation:get|put|post|deletepath: Subkeys for nested access (e.g.,["settings", "theme"])Return new value to allow/transform, or
Noneto hide/denyMatching: exact name first, then wildcard
"*"handlers
Example:
@app.property_hook("email")
def email_guard(actor, operation, value, path):
if operation in ("put", "post", "delete"):
return None # read-only
return value if actor.is_owner() else None # hide from non-owner
Callback Hooks
Decorator: app.callback_hook(name: str = "*")
Signature: func(actor, name: str, data: dict) -> bool | dict
Return
Truewhen processed; optionally return adictas response payloadUsed for custom endpoints registered by the framework (e.g.,
bot,www)
Template Rendering for WWW Hooks
The special www callback hook can render custom templates by returning a dict with template key:
@app.callback_hook("www")
def handle_www_paths(actor, name, data):
path = data.get("path", "")
if path == "custom":
return {
"template": "aw-actor-www-custom.html",
"data": {
"custom_message": "Hello from hook!",
},
}
return False # Fall through to default handling
The template will receive:
Standard values:
id,url,actor_root,actor_wwwCustom data from the
datadict in the return value
This allows applications to add custom web UI pages without modifying the core library.
Application Callback Hooks
Decorator: app.app_callback_hook(name: str)
Signature: func(data: dict) -> bool | dict
Like callback hooks but without actor context (application-level)
Subscription Hooks
Decorator: app.subscription_hook
Signature: func(actor, subscription: dict, peer_id: str, data: dict) -> bool
Return
Truewhen the subscription callback was handled
Lifecycle Hooks
Decorator: app.lifecycle_hook(event: str)
Signature: func(actor, **kwargs) -> Any
Common events:
actor_created,actor_deleted,oauth_success,trust_initiated,trust_request_received,trust_fully_approved_local,trust_fully_approved_remote,trust_deleted,subscription_deleted,email_verification_required,email_verified
Event Details
actor_createdTriggered when a new actor is created.
Signature:
func(actor: ActorInterface) -> Noneactor_deletedTriggered when an actor is deleted.
Signature:
func(actor: ActorInterface) -> Noneoauth_successTriggered after successful OAuth2 authentication.
Signature:
func(actor: ActorInterface, email: str, access_token: str, token_data: dict) -> Optional[bool]Returns:
Falseto reject authentication,TrueorNoneto accepttrust_initiatedTriggered when this actor initiates a trust request to another actor (outgoing request).
Signature:
func(actor: ActorInterface, peer_id: str, relationship: str, trust_data: dict) -> NoneParameters:
actor: The ActorInterface for the current actor (initiator)peer_id: The ID of the peer being invitedrelationship: The type of trust relationship requested (e.g., “friend”, “partner”)trust_data: Dictionary containing the trust relationship data
Use Cases: Logging, analytics, UI updates showing “request sent”
trust_request_receivedTriggered when this actor receives a trust request from another actor (incoming request).
Signature:
func(actor: ActorInterface, peer_id: str, relationship: str, trust_data: dict) -> NoneParameters:
actor: The ActorInterface for the current actor (recipient)peer_id: The ID of the peer who sent the requestrelationship: The type of trust relationship requested (e.g., “friend”, “partner”)trust_data: Dictionary containing the trust relationship data
Use Cases: Real-time notifications, UI popups, approval workflows
trust_fully_approved_localTriggered when THIS actor approves a trust relationship, completing mutual approval (both sides now approved).
Signature:
func(actor: ActorInterface, peer_id: str, relationship: str, trust_data: dict) -> NoneParameters:
actor: The ActorInterface for the current actor (who just approved)peer_id: The ID of the peer in the trust relationshiprelationship: The type of trust relationship (e.g., “friend”, “partner”)trust_data: Dictionary containing the full trust relationship data
Use Cases: UI notification “You approved! Relationship established”, analytics, triggering subscriptions
Note: Fires when this actor’s approval completes the mutual trust (peer had already approved).
Built-in Behavior: If peer profile caching is enabled (
with_peer_profile()), the peer’s profile is automatically fetched and cached when this hook fires.trust_fully_approved_remoteTriggered when the PEER actor approves a trust relationship, completing mutual approval (both sides now approved).
Signature:
func(actor: ActorInterface, peer_id: str, relationship: str, trust_data: dict) -> NoneParameters:
actor: The ActorInterface for the current actor (receiving notification)peer_id: The ID of the peer who just approvedrelationship: The type of trust relationship (e.g., “friend”, “partner”)trust_data: Dictionary containing the full trust relationship data
Use Cases: UI notification “They approved your request!”, analytics, triggering subscriptions
Note: Fires when the peer’s approval completes the mutual trust (this actor had already approved).
Built-in Behavior: If peer profile caching is enabled (
with_peer_profile()), the peer’s profile is automatically fetched and cached when this hook fires.trust_deletedTriggered when a trust relationship is deleted.
Signature:
func(actor: ActorInterface, peer_id: str, relationship: str, trust_data: dict) -> NoneParameters:
actor: The ActorInterface for the current actorpeer_id: The ID of the peer in the trust relationshiprelationship: The type of trust relationship (e.g., “friend”, “partner”)trust_data: Dictionary containing the trust relationship data (may be empty if trust was already deleted)
Built-in Behavior: If peer profile caching is enabled (
with_peer_profile()), the cached peer profile is automatically deleted when this hook fires.subscription_deletedTriggered when a subscription is deleted (inbound subscriptions only).
Signature:
func(actor: ActorInterface, peer_id: str, subscription_id: str, subscription_data: dict, initiated_by_peer: bool) -> NoneParameters:
actor: The ActorInterface for the current actorpeer_id: The ID of the peer in the subscriptionsubscription_id: The ID of the subscription that was deletedsubscription_data: Dictionary containing the subscription data (may be empty if subscription was already deleted)initiated_by_peer:Trueif the peer initiated the deletion (unsubscribed from us),Falseif we revoked their subscription
Use Cases: Revoke peer permissions, clean up cached data, send notifications when peers unsubscribe
Note: Only triggered for inbound subscriptions (where peer subscribes to us) to prevent duplicate cleanup. Outbound subscription deletions (us unsubscribing from peer) do not trigger this hook.
Example:
@app.lifecycle_hook("subscription_deleted") def on_subscription_deleted(actor, peer_id, subscription_id, subscription_data, initiated_by_peer): if initiated_by_peer: # Peer unsubscribed from us - revoke their permissions actor.trust.update_permissions(peer_id, []) notify_user(actor, f"{peer_id} unsubscribed from your data")
email_verification_requiredTriggered when email verification is needed for OAuth2 actors.
Signature:
func(actor: ActorInterface, email: str, verification_url: str, token: str) -> NonePurpose: Send verification email to user
Required: Must be implemented when using
with_email_as_creator(enable=True)Example:
@app.lifecycle_hook("email_verification_required") def send_verification_email(actor, email, verification_url, token): # Send email with verification_url to the user send_email( to=email, subject="Verify your email", body=f"Click here to verify: {verification_url}" )
email_verifiedTriggered when email verification is successfully completed.
Signature:
func(actor: ActorInterface, email: str) -> NonePurpose: Handle post-verification actions (welcome email, grant access, etc.)
Optional: Not required, but useful for tracking and analytics
Example:
@app.lifecycle_hook("email_verified") def handle_verification(actor, email): logger.info(f"Email verified: {email} for actor {actor.id}") # Optional: Send welcome email, enable features, etc.
Method Hooks
Decorator: app.method_hook(name, description="", input_schema=None, output_schema=None, annotations=None)
Signature: func(actor, method_name: str, data: dict) -> Any
Implements RPC-style methods under
/methods; first non-None return winsMetadata is exposed via
GET /<actor_id>/methodsfor API discovery
Metadata Parameters:
description: Human-readable description of what the method doesinput_schema: JSON schema describing expected input parametersoutput_schema: JSON schema describing the expected return valueannotations: Safety/behavior hints (e.g.,readOnlyHint,idempotentHint)
Example with Metadata:
@app.method_hook(
"calculate",
description="Perform a mathematical calculation",
input_schema={
"type": "object",
"properties": {"x": {"type": "number"}, "y": {"type": "number"}},
"required": ["x", "y"]
},
output_schema={"type": "object", "properties": {"result": {"type": "number"}}},
annotations={"readOnlyHint": True, "idempotentHint": True}
)
def handle_calculate(actor, method_name, data):
return {"result": data["x"] + data["y"]}
Auto-Generated Schemas from TypedDict:
If you don’t provide input_schema or output_schema explicitly, they can be
auto-generated from TypedDict type hints on your function:
from typing import TypedDict
class CalculateInput(TypedDict):
x: int
y: int
class CalculateOutput(TypedDict):
result: int
@app.method_hook("calculate", description="Add two numbers")
def handle_calculate(actor, method_name, data: CalculateInput) -> CalculateOutput:
return {"result": data["x"] + data["y"]}
The above automatically generates:
input_schemafrom thedataparameter’s TypedDict annotationoutput_schemafrom the return type annotation
Explicit schemas always take precedence over auto-generated ones. Supported types
include str, int, float, bool, list, dict, None,
Optional[...], and nested TypedDict classes.
Action Hooks
Decorator: app.action_hook(name, description="", input_schema=None, output_schema=None, annotations=None)
Signature: func(actor, action_name: str, data: dict) -> Any
Implements side-effecting operations under
/actions; first non-None return winsMetadata is exposed via
GET /<actor_id>/actionsfor API discovery
Metadata Parameters:
description: Human-readable description of what the action doesinput_schema: JSON schema describing expected input parametersoutput_schema: JSON schema describing the expected return valueannotations: Safety/behavior hints (e.g.,destructiveHint,readOnlyHint)
Example with Metadata:
@app.action_hook(
"delete_record",
description="Permanently delete a record from the database",
input_schema={
"type": "object",
"properties": {"record_id": {"type": "string"}},
"required": ["record_id"]
},
annotations={"destructiveHint": True, "readOnlyHint": False}
)
def handle_delete(actor, action_name, data):
delete_from_database(data["record_id"])
return {"status": "deleted"}
Auto-Generated Schemas from TypedDict:
Action hooks also support auto-schema generation from TypedDict type hints:
from typing import TypedDict
class DeleteInput(TypedDict):
record_id: str
class DeleteOutput(TypedDict):
status: str
@app.action_hook("delete_record", description="Delete a record")
def handle_delete(actor, action_name, data: DeleteInput) -> DeleteOutput:
delete_from_database(data["record_id"])
return {"status": "deleted"}
Async/Await Support
New in v3.9.0: All ActingWeb hooks now support native async/await syntax. This enables efficient handling of I/O-bound operations without blocking the event loop in async frameworks like FastAPI.
When to Use Async Hooks
Use async def for hooks that need to call async services:
Async HTTP clients (aiohttp, httpx)
Async database operations (asyncpg, motor)
Async AWS services (aioboto3, async AWS Bedrock)
Async AwProxy methods (
send_message_async(),fetch_property_async())Any async I/O operations
Performance Benefits
FastAPI: Async hooks execute natively without thread pool overhead, allowing true concurrent execution.
Flask: Async hooks are executed via asyncio.run(), providing compatibility with async libraries.
Async Method Hooks
import aiohttp
@app.method_hook("fetch_data")
async def async_fetch(actor, method_name, data):
"""Async method hook using aiohttp."""
async with aiohttp.ClientSession() as session:
async with session.get(data["url"]) as response:
content = await response.text()
return {"content": content, "status": response.status}
Async Action Hooks
from actingweb.interface import AwProxy
@app.action_hook("send_notification")
async def async_notify(actor, action_name, data):
"""Async action hook using AwProxy."""
proxy = AwProxy(config)
# Use async methods for peer communication
result = await proxy.send_message_async(
peer_url=data["peer_url"],
message=data["message"],
secret=actor.get_trust_secret(data["peer_id"])
)
return {"sent": result is not None}
Async Property Hooks
import asyncpg
@app.property_hook("user_profile")
async def async_property(actor, operation, value, path):
"""Async property hook with database access."""
if operation == "get":
# Fetch from async database
conn = await asyncpg.connect("postgresql://...")
profile = await conn.fetchrow(
"SELECT * FROM profiles WHERE actor_id = $1",
actor.id
)
await conn.close()
return dict(profile) if profile else None
return value
Async Lifecycle Hooks
@app.lifecycle_hook("actor_created")
async def async_onCreate(actor, **kwargs):
"""Async lifecycle hook for actor creation."""
# Initialize with async service
async with aiohttp.ClientSession() as session:
await session.post(
"https://analytics.example.com/events",
json={"event": "actor_created", "actor_id": actor.id}
)
Mixed Sync and Async Hooks
You can use both synchronous and asynchronous hooks in the same application:
@app.method_hook("quick_calc")
def sync_method(actor, method_name, data):
"""Synchronous CPU-bound operation."""
return {"result": data["x"] + data["y"]}
@app.method_hook("fetch_data")
async def async_method(actor, method_name, data):
"""Asynchronous I/O-bound operation."""
async with aiohttp.ClientSession() as session:
async with session.get(data["url"]) as resp:
return {"data": await resp.text()}
The framework automatically detects whether a hook is sync or async and handles execution appropriately.
Framework-Specific Behavior
- FastAPI:
Async hooks are executed natively without thread pool
Optimal for concurrent request handling
Uses
AsyncMethodsHandlerandAsyncActionsHandlerautomatically
- Flask:
Async hooks are executed via
asyncio.run()Compatible with async libraries but not truly concurrent
Falls back to standard
MethodsHandlerandActionsHandler
Best Practices
Use async for I/O: Network requests, database queries, file operations
Use sync for CPU: Calculations, data transformations, quick operations
Don’t mix unnecessarily: If all operations are sync, keep hooks sync
Test both paths: Ensure hooks work in both FastAPI and Flask contexts
Backward Compatibility
All existing synchronous hooks continue to work without changes. The async support is opt-in via async def syntax.
API Discovery
The GET /<actor_id>/methods and GET /<actor_id>/actions endpoints return
metadata for all registered hooks, enabling API discovery:
{
"methods": [
{
"name": "calculate",
"description": "Perform a mathematical calculation",
"input_schema": {"type": "object", "properties": {...}},
"output_schema": {"type": "object", "properties": {...}},
"annotations": {"readOnlyHint": true}
}
]
}
Hooks without metadata return default values (empty description, null schemas).
Matching & Ordering
Specific-name hooks execute before wildcard hooks
For methods/actions, the first hook returning a non-None value wins
For properties, hooks can transform the value;
Nonehides/denies
Path Semantics (Properties)
pathconveys nested segments. Example: GET/properties/settings/theme→path=["settings", "theme"]Use path to enforce fine-grained access or type normalization