Agent actions
Every entrypoint and task receives an Agent as its first argument. Each method on the agent is an action that runs on the RunLLM server on behalf of your assistant. RunLLM creates the agent for you, so you never construct one yourself.
Actions raise an Exception if the server request fails.
Several actions accept a ChatMessage, which can be any of:
- a plain
str - an
AssistantChatMessage, such as the return value ofagent.answer() - a
UserChatMessage, such asevent.conversation.new_message
Answering questions
answer()
agent.answer(target: Conversation) -> AssistantChatMessage
Generates an answer to target.new_message using your RunLLM assistant and its knowledge base. The answer is not sent anywhere. Pass it to one of the send_to_* actions to deliver it.
The returned message's category tells you how confident the assistant is. See AnswerCategory.
answer = agent.answer(event.conversation)
if answer.category == AnswerCategory.IRRELEVANT:
return
agent.send_to_slack_thread(answer, to=event.conversation.surface)
chat()
agent.chat(conversation: Conversation) -> AssistantChatMessage
Generates an answer for the conversation using the agentic RunLLM assistant, which can reason over the full conversation on the surface. Like answer(), it returns the message without sending it.
summarize()
agent.summarize(text: str, custom_instructions: str = "") -> str
Generates a structured summary (title, overview, implications, and so on) of any text, such as a conversation transcript, meeting notes, or Jira ticket details.
| Argument | Description |
|---|---|
| text | The text to summarize. |
| custom_instructions | Optional guidance for the summary, for example "Focus on business impact" or "Highlight action items". |
fetch_jira_tickets()
agent.fetch_jira_tickets(text: str, jira_domain_name: str) -> str
Finds Jira issue keys and URLs in text (for example PROJ-123 or https://acme.atlassian.net/browse/PROJ-123), fetches them from Jira, and returns a formatted string with each ticket's title, description, comments, and related issues. Returns an empty string if no tickets were found. Requires the Jira integration to be connected to your assistant.
details = agent.fetch_jira_tickets(event.conversation.new_message.text, "acme.atlassian.net")
if details:
summary = agent.summarize(details, "Focus on customer impact")
agent.send_to_slack_thread(summary, to=event.conversation.surface)
Tagging and categorizing
tag()
agent.tag(
conversation: Conversation,
options: Dict[str, str],
guidelines: Optional[str] = None,
) -> List[str]
Has the assistant pick every tag from options that fits the conversation, and applies them. Tags already on the conversation are skipped. Returns the newly selected tags, and adds them to conversation.tags.
| Argument | Description |
|---|---|
| conversation | The conversation to tag. |
| options | A mapping of tag name to a description of when the tag applies. |
| guidelines | Optional extra instructions for choosing tags. |
agent.tag(
event.conversation,
options={
"billing": "Questions about invoices, pricing, or payment methods.",
"bug": "The user is reporting something that looks broken.",
},
)
categorize()
agent.categorize(
conversation: Conversation,
categories: Dict[str, str],
guidelines: Optional[str] = None,
) -> str
Has the assistant pick exactly one category from categories that best describes the conversation, and returns its name. Unlike tag(), this doesn't apply anything to the conversation. Call apply_tag() if you want to record it.
Because a category is always chosen, include a catch-all such as "other" for conversations that don't fit any of your categories.
category = agent.categorize(
event.conversation,
categories={
"how_to": "The user wants to know how to do something.",
"incident": "The user is reporting an outage or degraded service.",
"other": "Anything else.",
},
)
agent.apply_tag(category, to=event.conversation)
apply_tag()
agent.apply_tag(tag: str, to: Conversation) -> None
Applies a single tag to the conversation, and adds it to conversation.tags.
Slack
send_to_slack_thread()
agent.send_to_slack_thread(
message: ChatMessage,
to: SlackThread,
ephemeral: bool = False,
prefix: Optional[str] = None,
enable_feedback: bool = True,
blocks: Optional[List[Dict[str, Any]]] = None,
) -> Optional[ButtonClicked]
Posts a message to a Slack thread. If to is a new thread returned by create_or_get_slack_thread() that hasn't been posted to yet, the first send creates the thread and sets its thread_ts.
| Argument | Description |
|---|---|
| message | The message to post. |
| to | The SlackThread to post to. |
| ephemeral | If True, posts an ephemeral message that only the user can see. Defaults to False. |
| prefix | Optional text to put before the message. |
| enable_feedback | If True, adds RunLLM's feedback footer to the message. Defaults to True. |
| blocks | Optional extra Slack blocks to add after the message. Markdown blocks are always sent with verbatim set to true. |
In addition to standard Slack blocks, blocks supports a RunLLM-specific buttons block. Each button has a text, and either an id (a button your workflow responds to, with an optional style of "primary" or "danger") or a url (a link button):
clicked = agent.send_to_slack_thread(
"Did this answer your question?",
to=thread,
enable_feedback=False,
blocks=[
{
"type": "buttons",
"buttons": [
{"id": "yes", "style": "primary", "text": "Yes"},
{"id": "no", "style": "danger", "text": "No"},
{"text": "Read the docs", "url": "https://docs.example.com"},
],
}
],
)
if clicked and clicked.id == "no":
...
When a message includes buttons with an id, the task suspends until the user clicks one, and then re-runs with send_to_slack_thread() returning a ButtonClicked whose id is the clicked button. Otherwise, it returns None. Read Buttons and suspended execution before using buttons.
create_or_get_slack_thread()
agent.create_or_get_slack_thread(
session_id: int,
team_id: str,
channel_id: str,
) -> SlackThread
Returns a Slack thread in channel_id that's linked to the conversation session_id. Calling it again with the same arguments returns the same thread, so it's safe to call on every run. The thread isn't posted to Slack until you first call send_to_slack_thread() with it.
Use this to escalate conversations from any surface to an internal Slack channel.
| Argument | Description |
|---|---|
| session_id | The conversation the thread belongs to, usually event.conversation.session_id. |
| team_id | The Slack workspace ID (starts with T). |
| channel_id | The Slack channel ID (starts with C). The RunLLM bot must be a member of the channel. |
Chat widget
send_to_chat_widget()
agent.send_to_chat_widget(message: ChatMessage, to: ChatWidget) -> None
Sends a message to a chat widget conversation.
answer = agent.answer(event.conversation)
agent.send_to_chat_widget(answer, to=event.conversation.surface)
Zendesk
create_zendesk_ticket()
agent.create_zendesk_ticket(
subdomain: str,
conversation: Conversation,
tags: Optional[List[str]] = None,
title: Optional[str] = None,
description: Optional[str] = None,
followup_source_id: Optional[str] = None,
) -> ZendeskTicket
Creates a Zendesk ticket containing the conversation transcript, and returns it.
| Argument | Description |
|---|---|
| subdomain | Your Zendesk subdomain. For https://acme.zendesk.com, use acme. |
| conversation | The conversation to put in the ticket. |
| tags | Optional Zendesk tags to apply to the ticket. |
| title | Optional ticket subject. Defaults to the first user message in the conversation. |
| description | Optional template for the ticket body. Use {conversation} where the transcript should go, for example "Escalated from the web widget:\n\n{conversation}". |
| followup_source_id | Optional ID of a closed ticket that this ticket follows up on. |
send_to_zendesk_ticket()
agent.send_to_zendesk_ticket(
message: ChatMessage,
to: ZendeskTicket,
requester_email: Optional[str] = None,
public: bool = False,
) -> None
Adds a comment to a Zendesk ticket.
| Argument | Description |
|---|---|
| message | The comment to add. |
| to | The ZendeskTicket to comment on. |
| requester_email | Optional email address to post the comment as. Defaults to bot@runllm.com. |
| public | If True, the comment is visible to the requester. Defaults to False (internal note). |
update_zendesk_ticket()
agent.update_zendesk_ticket(
ticket: ZendeskTicket,
status: Optional[str] = None,
custom_fields: Optional[List[ZendeskCustomField]] = None,
) -> None
Updates a ticket's status and custom fields. Use ZendeskTicketStatus for the built-in statuses. On success, ticket.status is updated in place.
from runllm import ZendeskCustomField, ZendeskTicketStatus
agent.update_zendesk_ticket(
ticket,
status=ZendeskTicketStatus.PENDING,
custom_fields=[ZendeskCustomField(id=360012345678, value="ai_answered")],
)
Workflow control
listen()
agent.listen(
on: List[ChatSurface],
handler: TaskWrapper,
triggers: Optional[List[Trigger]] = None,
) -> None
Moves the workflow run to the handler task and starts listening for events on the surfaces in on. The current function keeps running until it returns, but future events in the conversation are routed to handler instead of the current task. See Moving between tasks.
| Argument | Description |
|---|---|
| on | The surfaces to listen on. They're passed to handler as positional arguments in the same order. |
| handler | The @task-decorated function to call on the next matching event. It must be included in Client.publish(..., tasks=[...]). |
| triggers | One trigger per surface in on, in the same order. The lists must be the same length. |
triggersPass triggers explicitly. Use ConvoMessage() for Slack threads and chat widgets, and TicketComment() for Zendesk tickets.
agent.listen(
on=[event.conversation.surface, ticket],
handler=wait_for_agent_reply,
triggers=[ConvoMessage(), TicketComment()],
)
edit_message()
agent.edit_message(convo: Conversation, text: str) -> None
Replaces the text of convo.new_message in RunLLM, and updates convo.new_message.text in place. This is useful for normalizing or enriching a question before calling answer(), for example by appending the details returned by fetch_jira_tickets().
save_state()
agent.save_state(state: Dict[str, Any]) -> None
Saves each key-value pair in state. Values must be JSON-serializable. Existing keys are overwritten. State is shared by all runs of the workflow.
read_state()
agent.read_state(key: str) -> Any
Returns the value saved under key, or None if it has never been set.