Agent actions
Every entrypoint and task receives an Agent object as its first argument. Each method on the agent is an action that runs on the Herald server on behalf of your assistant. Herald 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
Section titled “Answering questions”answer()
Section titled “answer()”agent.answer(target: Conversation) -> AssistantChatMessageGenerates an answer to target.new_message using your Herald 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: returnagent.send_to_slack_thread(answer, to=event.conversation.surface)chat()
Section titled “chat()”agent.chat(conversation: Conversation) -> AssistantChatMessageGenerates an answer for the conversation using the agentic Herald assistant, which can reason over the full conversation on the surface. Like answer(), it returns the message without sending it.
summarize()
Section titled “summarize()”agent.summarize(text: str, custom_instructions: str = "") -> strGenerates 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()
Section titled “fetch_jira_tickets()”agent.fetch_jira_tickets(text: str, jira_domain_name: str) -> strFinds 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
Section titled “Tagging and categorizing”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()
Section titled “categorize()”agent.categorize( conversation: Conversation, categories: Dict[str, str], guidelines: Optional[str] = None,) -> strHas 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.
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()
Section titled “apply_tag()”agent.apply_tag(tag: str, to: Conversation) -> NoneApplies a single tag to the conversation, and adds it to conversation.tags.
send_to_slack_thread()
Section titled “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 Herald’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 Herald-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()
Section titled “create_or_get_slack_thread()”agent.create_or_get_slack_thread( session_id: int, team_id: str, channel_id: str,) -> SlackThreadReturns 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 Herald bot must be a member of the channel. |
Chat widget
Section titled “Chat widget”send_to_chat_widget()
Section titled “send_to_chat_widget()”agent.send_to_chat_widget(message: ChatMessage, to: ChatWidget) -> NoneSends a message to a chat widget conversation.
answer = agent.answer(event.conversation)agent.send_to_chat_widget(answer, to=event.conversation.surface)Zendesk
Section titled “Zendesk”create_zendesk_ticket()
Section titled “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,) -> ZendeskTicketCreates 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()
Section titled “send_to_zendesk_ticket()”agent.send_to_zendesk_ticket( message: ChatMessage, to: ZendeskTicket, requester_email: Optional[str] = None, public: bool = False,) -> NoneAdds 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()
Section titled “update_zendesk_ticket()”agent.update_zendesk_ticket( ticket: ZendeskTicket, status: Optional[str] = None, custom_fields: Optional[List[ZendeskCustomField]] = None,) -> NoneUpdates 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
Section titled “Workflow control”listen()
Section titled “listen()”agent.listen( on: List[ChatSurface], handler: TaskWrapper, triggers: Optional[List[Trigger]] = None,) -> NoneMoves 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. |
agent.listen( on=[event.conversation.surface, ticket], handler=wait_for_agent_reply, triggers=[ConvoMessage(), TicketComment()],)edit_message()
Section titled “edit_message()”agent.edit_message(convo: Conversation, text: str) -> NoneReplaces the text of convo.new_message in Herald, 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()
Section titled “save_state()”agent.save_state(state: Dict[str, Any]) -> NoneSaves 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()
Section titled “read_state()”agent.read_state(key: str) -> AnyReturns the value saved under key, or None if it has never been set.