AI agents are becoming a new interface for handling inbound messages.
Instead of opening an inbox, scanning dozens of conversations, deciding which ones matter, and manually typing replies, a user can now say something like:
Check my recent messages for new leads.
In
The Lead Engagement process is the passive filtering pipeline. While the Search and Contact process proactively finds people, Lead Engagement handles the messages that have already arrived.
The agent analyses all recent messages and classifies each conversation into one of four actions:
| Scenario | Action |
|---|---|
| Worth following up, fewer than 10 messages | Reply in the current chat |
| Worth following up, 10 or more messages | Compact the context and create a new chat with a reply |
| Not suitable for follow-up yet | Take no action for now |
| Completely irrelevant (e.g., marketing spam) | Quit the space to avoid being disturbed |
This classification is not arbitrary. The 10-message threshold exists because long chat threads accumulate noise. An AI agent reading a 50-message thread must parse through greetings, tangents, and outdated context before finding the signal. By forking into a new chat at the 10-message mark, the agent compresses the history into a compact summary and starts fresh — a design choice we will revisit later.
The agent-side entry points
The Lead Engagement process exposes five functions to the AI agent. They are implemented in the skill's scripts/callable_functions.py file and communicate with QuestMeet through GraphQL.
Reading functions
def ai_read_messages(access_token: str, lookback_window: int = None) -> Union[list, bool, None]:
try:
response = httpx.post(
BASE_URL,
json={
"query": """
query AiReadMessages($lookbackWindow: Int) {
aiReadMessages(lookbackWindow: $lookbackWindow)
}
""",
"variables": {"lookbackWindow": lookback_window},
},
headers={"Authorization": f"Bearer {access_token}"},
trust_env=False,
timeout=20,
)
return response.json()["data"]["aiReadMessages"]
except Exception:
return False
def ai_read_chat_messages(access_token: str, space_id: str, chat_id: str) -> Union[dict, bool, None]:
try:
response = httpx.post(
BASE_URL,
json={
"query": """
query AiReadChatMessages($spaceId: BigInt!, $chatId: BigInt!) {
aiReadChatMessages(spaceId: $spaceId, chatId: $chatId)
}
""",
"variables": {"spaceId": space_id, "chatId": chat_id},
},
headers={"Authorization": f"Bearer {access_token}"},
trust_env=False,
timeout=20,
)
return response.json()["data"]["aiReadChatMessages"]
except Exception:
return False
Writing functions
def ai_create_message(access_token: str, space_id: str, chat_id: str, content: str) -> Union[bool, None]:
try:
response = httpx.post(
BASE_URL,
json={
"query": """
mutation AiCreateMessage($spaceId: BigInt!, $chatId: BigInt!, $content: String!) {
aiCreateMessage(spaceId: $spaceId, chatId: $chatId, content: $content)
}
""",
"variables": {"spaceId": space_id, "chatId": chat_id, "content": content},
},
headers={"Authorization": f"Bearer {access_token}"},
trust_env=False,
timeout=20,
)
return response.json()["data"]["aiCreateMessage"]
except Exception:
return False
def ai_create_chat_and_message(access_token: str, space_id: str, content: str) -> Union[bool, None]:
try:
response = httpx.post(
BASE_URL,
json={
"query": """
mutation AiCreateChatAndMessage($spaceId: BigInt!, $content: String!) {
aiCreateChatAndMessage(spaceId: $spaceId, content: $content)
}
""",
"variables": {"spaceId": space_id, "content": content},
},
headers={"Authorization": f"Bearer {access_token}"},
trust_env=False,
timeout=20,
)
return response.json()["data"]["aiCreateChatAndMessage"]
except Exception:
return False
Cleanup function
def ai_quit_spaces(access_token: str, space_ids: list[str]) -> Union[bool, None]:
try:
response = httpx.post(
BASE_URL,
json={
"query": """
mutation AiQuitSpaces($spaceIds: [BigInt!]!) {
aiQuitSpaces(spaceIds: $spaceIds)
}
""",
"variables": {"spaceIds": space_ids},
},
headers={"Authorization": f"Bearer {access_token}"},
trust_env=False,
timeout=20,
)
return response.json()["data"]["aiQuitSpaces"]
except Exception:
return False
Return value semantics
All five functions share the same return value contract:
| Return value | Meaning |
|---|---|
list[dict] / dict / True | The operation succeeded |
[] | The read succeeded, but no relevant messages were found |
None | The access token is missing or expired; the agent should re-authenticate |
False | Something failed; notify the user and stop |
This contract is critical because the agent, not the server, owns the workflow. If the token is expired, the skill instructs the agent to run the sign-in process, obtain a new token, and retry. The server never attempts to redirect or refresh — it simply returns None and lets the agent decide.
The data model
The Lead Engagement functions touch five tables:
usersspaces
members/copy_members
chatsmessages
Here is the simplified relationship:
SOCIAL SHARE CARD GENERATOR