BaseChatbotWriter is the recommended starting point for any chatbot destination (Intercom, Zendesk, Salesforce, custom LLM agent, etc.). Subclass it, implement two async methods, and you get the full delivery policy for free.
What it does
Pre-link buffering (sliding window)
Before a conversation is linked to a PostHog session, actions are buffered in memory rather than discarded. On every new arrival, entries older thanpre_link_window_s seconds are trimmed — so the buffer never grows unboundedly.
At-link flush (one API call)
Whenon_session_linked() fires, the entire buffer is flushed as a single _post_note call. Actions are grouped into bin_seconds-wide visual bins separated by blank lines — making the user’s journey easy to scan. One API call regardless of how many events accumulated before the conversation opened.
Post-link debounce (trailing edge)
After a conversation is linked, eachwrite_actions() call appends to a per-session buffer and (re)schedules a short asyncio.Task. When the timer fires with no new arrivals, one _post_note is made. Rapid event bursts are coalesced; a pause longer than post_link_debounce_s triggers delivery.
Note body format
BaseChatbotWriter builds the string passed to _post_note(conversation_id, body) as plain text, line-oriented.
Header (always)
The first lines come fromformat_chatbot_note_header(session_id, timestamp_unix):
session_id: …timestamp: … UTC(human-readable from Unix time)- A blank line after the header
timestamp_start among the actions after sorting. If slim_actions is empty, the header uses the current time instead.
Action lines
- Actions are sorted by
timestamp_startbefore rendering. - Each line is
[n] {description}wherenis 1-based and local to that note. It is not the per-batch wireindexon each slim action dict. - One action → one line
[1] …. Many →[1]through[n]. Zero actions → header only (no action lines).
Binning
- Pre-link flush (
on_session_linked): uses the constructor’sbin_seconds(default3). Actions whosetimestamp_startvalues fall in different time bins get a blank line between groups so the note is easier to scan. - Post-link debounced notes:
_format_noteis called withbin_seconds=0, so no extra blank lines between groups.
Example (actions note)
[2] and [3] appears when those actions fall in different bin_seconds-wide bins (pre-link flush). Post-link notes omit those separators.
Summary notes (LLM)
_format_note applies only to action timelines. BaseChatbotWriter does not wrap LLM summary text.
For overwrite_with_summary, build the body yourself: call format_chatbot_note_header(session_id, time.time()) (or another Unix timestamp), then append your summary prose. The in-repo Intercom integration does this for summary posts.
Constructor
BaseChatbotWriter(product_id, pre_link_window_s=120, post_link_debounce_s=0.15, bin_seconds=3)
Product identifier used in logs and metrics.
How long to retain buffered actions before the session is linked. Actions older than this (measured by
timestamp_start) are dropped on each new arrival. Default is 120 seconds (2 minutes).Trailing-edge debounce window in seconds. After the last
write_actions() call for a session, the writer waits this long before posting a note. Coalesces rapid event bursts into a single API call. Default is 150 ms.Width of time bins used to group actions into visual sections in the note body. Actions more than
bin_seconds apart get a blank-line separator. Set to 0 to disable binning. Used for the pre-link flush note; post-link notes always use bin_seconds=0.Building a custom backend
SubclassBaseChatbotWriter and implement these two methods:
_post_note(conversation_id, body) → str | None
Post a note to the platform conversation. Return its platform-assigned id (used for later redaction), or None if the platform does not support redaction.
_redact_part(conversation_id, part_id) → None
Delete or blank a previously posted note. This is called by AsyncAgentContextWriter’s overwrite_with_summary step when LLM summaries are enabled. Implement as a no-op if the platform does not support redaction.
Example — Zendesk
Using with AsyncAgentContextWriter
BaseChatbotWriter already debounces write_actions() calls internally via post_link_debounce_s. When wiring an AsyncAgentContextWriter to a BaseChatbotWriter subclass, keep debounce_ms=0 (the default) — the base class debounce is sufficient, and stacking both windows only adds latency.
API reference
| Method | Description |
|---|---|
write_actions(conversation_id, session_id, slim_actions, ...) | Route actions to pre-link buffer or post-link debounce pipeline |
on_session_linked(session_id, conversation_id) | Store the session→conversation mapping and flush pre-link buffer |
_post_note(conversation_id, body) | Subclass contract — post a note; return its id or None |
_redact_part(conversation_id, part_id) | Subclass contract — delete/blank a posted note (best-effort) |
_format_note(session_id, slim_actions, bin_seconds=None) | Builds the action-note body described in Note body format above (header, sorted 1-based lines, optional binning) |
Related
- AgentContextWriter — LLM summarisation and push delivery; pairs with
BaseChatbotWriterfor the full pipeline - Typed payloads —
ActionsPayloadandSlimAction— the typed models your callbacks receive - Intercom integration — the built-in
BaseChatbotWritersubclass for Intercom