Publishers

In shortReference for EventModel, BasePublisher, the Console, Redis, Kafka, RabbitMQ, OTEL, Logfire and LangSmith publishers, and publish_event.

  • 15 min read
  • 19 sections
  • Updated
  • v0.10.0
  • Markdown

Publishers receive an EventModel for every graph, node, LLM, tool and streaming step, so you can send execution events to a logger, a message bus or a tracing backend. This page lists each publisher class, its configuration, its defaults, and the publish_event helper for custom events. For a task-oriented walkthrough, see Use publishers; for the concepts, see Events and observability.

Import paths

imports.py
from tenxgraph.core.graph import StateGraph
from tenxgraph.runtime.publisher import BasePublisher, ConsolePublisher
from tenxgraph.runtime.publisher.events import Event, EventType, ContentType, EventModel
from tenxgraph.runtime.publisher import publish_event

# Optional backends
from tenxgraph.runtime.publisher import RedisPublisher     # pip install "10xgraph[redis]"
from tenxgraph.runtime.publisher import KafkaPublisher     # pip install "10xgraph[kafka]"
from tenxgraph.runtime.publisher import RabbitMQPublisher  # pip install "10xgraph[rabbitmq]"

# Fan-out
from tenxgraph.runtime.publisher import CompositePublisher

# Tracing backends
from tenxgraph.runtime.publisher import (
    ObservabilityLevel,
    OtelPublisher,
    LogfirePublisher,
    LangsmithPublisher,
    setup_tracing,
    setup_logfire,
    setup_langsmith,
    setup_observability,
)

EventModel

EventModel is the pydantic model every publisher receives. The runtime emits one at each significant moment: node start and end, LLM call, tool call, streaming chunk and error. Enum fields are stored as their string values (use_enum_values).

Signature:

Python
class EventModel(BaseModel):
    event: Event
    event_type: EventType
    content: str = ""
    content_blocks: list[ContentBlock] | None = None
    data: dict[str, Any] = {}
    content_type: list[ContentType] | None = None
    node_name: str = ""
    run_id: str  # default: new UUID4 string
    thread_id: str | int = ""
    user_id: str | int | None = None
    timestamp: float  # default: current time
    is_error: bool = False
    metadata: dict[str, Any] = {}
Parameter Type Default Description
event Event (required) Source: GRAPH_EXECUTION, NODE_EXECUTION, LLM_CALL, TOOL_EXECUTION, STREAMING, or REALTIME.
event_type EventType (required) Phase: START, PROGRESS, RESULT, END, UPDATE, ERROR, or INTERRUPTED.
content str "" Streamed textual content.
content_blocks list[ContentBlock] | None None Structured content blocks (TextBlock, ImageBlock, etc.) for multimodal or structured streaming.
data dict[str, Any] {} Structured payload carrying event details.
content_type list[ContentType] | None None Semantic types of the payload (may be a list).
node_name str "" Name of the graph node that produced this event.
run_id str new UUID4 string Unique ID for the execution run.
thread_id str | int "" Thread ID for resumable execution.
user_id str | int | None None User ID associated with the execution (from config).
timestamp float current time UNIX timestamp when the event was created.
is_error bool False True when the event represents an error state.
metadata dict[str, Any] {} Additional context for consumers (run_timestamp, is_stream, etc.).

Example:

Python
from tenxgraph.runtime.publisher.events import EventModel, Event, EventType, ContentType

# The runtime normally creates these; build one by hand for custom events
event = EventModel(
    event=Event.NODE_EXECUTION,
    event_type=EventType.START,
    node_name="agent",
    run_id="run-123",
    thread_id="thread-456",
    content_type=[ContentType.MESSAGE],
    data={"model": "gpt-4", "temperature": 0.7},
)

Event - source enum

Where the event originated.

Value Description
GRAPH_EXECUTION Emitted by the graph runner (start/end of full invocation).
NODE_EXECUTION Emitted at the start and end of each node run.
LLM_CALL Emitted for individual LLM API calls within a node.
TOOL_EXECUTION Emitted before and after each tool call.
STREAMING Emitted for each incremental streaming chunk from the LLM.
REALTIME Emitted by realtime audio-to-audio sessions.

EventType - phase enum

What phase of execution this event represents.

Value When emitted
START Execution begins.
PROGRESS Intermediate update during streaming (incremental chunk).
RESULT A result is ready (tool result, LLM completion).
END Execution ends (success or error).
UPDATE State or data updated.
ERROR An error occurred.
INTERRUPTED Execution paused at an interrupt point.

ContentType - payload type enum

Semantic label for what is in the event payload.

Value When used
TEXT Plain text output or content.
MESSAGE Full Message object.
REASONING Extended thinking trace or reasoning.
TOOL_CALL Tool invocation request (from LLM).
TOOL_RESULT Tool execution result.
IMAGE Image content.
AUDIO Audio content.
TRANSCRIPT Text transcript of audio (realtime sessions).
VIDEO Video content.
DOCUMENT Document content.
DATA Binary or structured data.
STATE Graph state snapshot.
UPDATE Incremental state update.
ERROR Error payload or error details.

BasePublisher

BasePublisher is the abstract base class for all publishers. Subclasses implement publish, close and sync_close. It stores the config dict as self.config, tracks closed state in self._is_closed, and works as an async context manager that calls close() on exit.

Signature:

Python
class BasePublisher(ABC):
    def __init__(self, config: dict[str, Any])

    @abstractmethod
    async def publish(self, event: EventModel) -> Any
    
    @abstractmethod
    async def close(self) -> None

    @abstractmethod
    def sync_close(self) -> None
Method Returns Raises Description
publish(event) Any RuntimeError (built-in publishers) Publish one event. Built-in publishers raise if the publisher is closed.
close() None - Release connections and resources (idempotent, async).
sync_close() None - Release connections and resources (idempotent, synchronous).

Example:

context_manager.py
import asyncio

from tenxgraph.core.graph import StateGraph
from tenxgraph.core.state import AgentState, Message
from tenxgraph.runtime.publisher import ConsolePublisher
from tenxgraph.utils.constants import END, START


def echo(state: AgentState) -> list[Message]:
    return [Message.text_message("hello", role="assistant")]


async def main() -> None:
    # The publisher is closed automatically when the block exits
    async with ConsolePublisher() as publisher:
        graph = StateGraph(publisher=publisher)
        graph.add_node("echo", echo)
        graph.add_edge(START, "echo")
        graph.add_edge("echo", END)
        app = graph.compile()
        await app.ainvoke({"messages": [Message.text_message("hi")]})


asyncio.run(main())

ConsolePublisher

ConsolePublisher prints each event as one line to stdout, or sends it to the tenxgraph.publisher logger at INFO level. It is for development and debugging; use a real transport in production.

Signature:

Python
class ConsolePublisher(BasePublisher):
    def __init__(self, config: dict[str, Any] | None = None)
Config key Type Default Description
use_logger bool False When True, emit through the tenxgraph.publisher logger at INFO instead of print.
format str "json" Stored on the instance. The current output is a single text line regardless of this value.
include_timestamp bool True Stored on the instance. The timestamp is always part of the line.
indent int 2 Stored on the instance. Not used by the current output.

Each line has the form <timestamp> -> Source: <node_name>.<event_type>:-> Payload: <data> -> <metadata>. publish raises RuntimeError after close().

Example:

console_publisher.py
from tenxgraph.core.graph import StateGraph
from tenxgraph.runtime.publisher import ConsolePublisher

# Print events to stdout
publisher = ConsolePublisher()

# Or route them through the logger, so they follow your logging config
publisher = ConsolePublisher(config={"use_logger": True})

graph = StateGraph(publisher=publisher)

RedisPublisher

RedisPublisher sends each event as JSON to a Redis Pub/Sub channel (default) or appends it to a Redis Stream. It connects lazily on the first publish, using a connection pool and a ping check.

Requires: pip install "10xgraph[redis]"

Signature:

Python
class RedisPublisher(BasePublisher):
    def __init__(self, config: dict[str, Any] | None = None)
Config key Type Default Description
url str redis://localhost:6379/0 Redis connection URL.
mode str "pubsub" "pubsub" for Pub/Sub, "stream" for Redis Streams (XADD).
channel str tenxgraph.events Pub/Sub channel (pubsub mode).
stream str tenxgraph.events Stream key (stream mode).
maxlen int | None None Approximate maximum stream length (stream mode). Unbounded when unset.
encoding str "utf-8" Payload encoding.
max_connections int 10 Connection pool size.
socket_timeout float 5.0 Socket timeout in seconds.
socket_connect_timeout float 5.0 Connect timeout in seconds.
socket_keepalive bool True Enable TCP keepalive.
health_check_interval int 30 Connection health check interval in seconds.

Returns: from publish, the subscriber count (pubsub) or the stream entry ID (stream). In stream mode the JSON payload is stored in the entry’s data field.

Raises: RuntimeError if the publisher is closed, the redis package is missing, or the connection fails.

Example:

redis_publisher.py
from tenxgraph.core.graph import StateGraph
from tenxgraph.runtime.publisher import RedisPublisher

# Pub/Sub on a named channel
pubsub = RedisPublisher(config={"url": "redis://localhost:6379/0", "channel": "agent-events"})

# Stream mode with a bounded length
stream = RedisPublisher(config={"mode": "stream", "stream": "agent-events", "maxlen": 10000})

graph = StateGraph(publisher=stream)

KafkaPublisher

KafkaPublisher sends each event as a UTF-8 JSON message to a Kafka topic with aiokafka, waiting for the broker acknowledgement. The producer starts lazily on the first publish.

Requires: pip install "10xgraph[kafka]"

Signature:

Python
class KafkaPublisher(BasePublisher):
    def __init__(self, config: dict[str, Any] | None = None)
Config key Type Default Description
bootstrap_servers str localhost:9092 Kafka bootstrap servers.
topic str tenxgraph.events Topic to publish to.
client_id str | None None Producer client ID.
max_batch_size int 16384 Maximum batch size in bytes.
linger_ms int 0 Time to wait for more messages before sending a batch.
compression_type str | None None Passed to aiokafka (for example "gzip").
request_timeout_ms int 30000 Request timeout in milliseconds.

Returns: from publish, the result of send_and_wait. Raises: RuntimeError if closed or aiokafka is missing.

Example:

kafka_publisher.py
from tenxgraph.core.graph import StateGraph
from tenxgraph.runtime.publisher import KafkaPublisher

publisher = KafkaPublisher(config={
    "bootstrap_servers": "localhost:9092",
    "topic": "agent-events",
    "compression_type": "gzip",
})

graph = StateGraph(publisher=publisher)

RabbitMQPublisher

RabbitMQPublisher sends each event as a JSON message to a RabbitMQ exchange with a routing key, using aio-pika and a robust (auto-reconnecting) connection. It connects lazily on the first publish.

Requires: pip install "10xgraph[rabbitmq]"

Signature:

Python
class RabbitMQPublisher(BasePublisher):
    def __init__(self, config: dict[str, Any] | None = None)
Config key Type Default Description
url str amqp://guest:guest@localhost/ RabbitMQ connection URL.
exchange str tenxgraph.events Exchange name.
routing_key str tenxgraph.events Routing key for messages.
exchange_type str "topic" Exchange type (for example "topic", "direct", "fanout"). Unknown values fall back to topic.
declare bool True Declare the exchange. When False, the default exchange is used.
durable bool True Declare the exchange as durable.
connection_timeout int 10 Connect timeout in seconds.
heartbeat int 60 Heartbeat interval in seconds.

Returns: True from publish. Raises: RuntimeError if closed or aio-pika is missing.

Example:

rabbitmq_publisher.py
from tenxgraph.core.graph import StateGraph
from tenxgraph.runtime.publisher import RabbitMQPublisher

publisher = RabbitMQPublisher(config={
    "url": "amqp://guest:guest@localhost:5672/",
    "exchange": "agent-events",
    "routing_key": "graph.events",
})

graph = StateGraph(publisher=publisher)

CompositePublisher

CompositePublisher fans each event out to several publishers concurrently with asyncio.gather. Exceptions from one publisher are swallowed (return_exceptions=True) so the others still receive the event. StateGraph(publisher=[...]) with a list builds one for you.

Signature:

Python
class CompositePublisher(BasePublisher):
    def __init__(self, publishers: list[BasePublisher])
    def add_publisher(self, publisher: BasePublisher) -> None
    def remove_publisher(self, publisher: BasePublisher) -> None
Method Returns Description
add_publisher(publisher) None Append a publisher to the fan-out list.
remove_publisher(publisher) None Remove a publisher. Raises ValueError if it is not in the list.
close() None Close every child publisher concurrently.
sync_close() None Close every child publisher synchronously, in order.

Example:

composite_publisher.py
from tenxgraph.core.graph import StateGraph
from tenxgraph.runtime.publisher import CompositePublisher, ConsolePublisher, RedisPublisher

publisher = CompositePublisher([
    ConsolePublisher(),
    RedisPublisher({"url": "redis://localhost:6379/0"}),
])

graph = StateGraph(publisher=publisher)
# Equivalent: StateGraph(publisher=[ConsolePublisher(), RedisPublisher(...)])

ObservabilityLevel

ObservabilityLevel is a string enum that controls how much data the tracing publishers put on spans. The default is STANDARD; use FULL only where prompts and responses may be stored.

Value String Captures
SPANS "spans" Timing and structure only, no I/O data.
STANDARD "standard" Adds token counts, model name and request parameters when available. Default.
FULL "full" Adds model input and output messages, tool I/O and the system prompt. May contain PII.

OtelPublisher

OtelPublisher turns EventModel events into OpenTelemetry spans, tracking parent and child spans explicitly. Attach it before compile(), because the publisher is bound into the dependency container at compile time.

Requires: pip install "10xgraph[otel]"

Signature:

Python
class OtelPublisher(BasePublisher):
    def __init__(
        self,
        tracer: Tracer | None = None,
        level: ObservabilityLevel = ObservabilityLevel.STANDARD,
    )

def setup_tracing(
    graph: StateGraph,
    tracer: Tracer | None = None,
    level: ObservabilityLevel = ObservabilityLevel.STANDARD,
) -> OtelPublisher
Parameter Type Default Description
graph StateGraph (required) setup_tracing only. Graph to instrument.
tracer Tracer | None None Explicit tracer. Uses the global TracerProvider (tracer name 10xgraph) when omitted.
level ObservabilityLevel STANDARD Span detail level.

Returns: setup_tracing returns the OtelPublisher it assigned to the graph. Raises: ImportError if opentelemetry-api is not installed.

Example:

otel_tracing.py
from tenxgraph.core.graph import StateGraph
from tenxgraph.runtime.publisher import ObservabilityLevel, setup_tracing

graph = StateGraph()
# ... add nodes and edges

# Attach tracing before compile
setup_tracing(graph, level=ObservabilityLevel.STANDARD)
app = graph.compile()

LogfirePublisher and setup_logfire

LogfirePublisher is an OtelPublisher that calls logfire.configure() on construction and exports spans to Logfire. setup_logfire does the same configuration and also instruments a graph with setup_tracing.

Requires: pip install "10xgraph[logfire]"

Signature:

Python
class LogfirePublisher(OtelPublisher):
    def __init__(
        self,
        *,
        token: str | None = None,
        service_name: str | None = None,
        send_to_logfire: bool = True,
        console: Any = None,
        level: ObservabilityLevel = ObservabilityLevel.STANDARD,
        additional_span_processors: list | None = None,
        **configure_kwargs: Any,
    )

def setup_logfire(
    graph: StateGraph | None = None,
    *,
    token: str | None = None,
    service_name: str | None = None,
    send_to_logfire: bool = True,
    console: Any = None,
    level: ObservabilityLevel = ObservabilityLevel.STANDARD,
    additional_span_processors: list | None = None,
    **configure_kwargs: Any,
) -> None
Parameter Type Default Description
graph StateGraph | None None setup_logfire only. None configures Logfire without attaching a publisher.
token str | None None Logfire write token. Falls back to the LOGFIRE_TOKEN environment variable.
service_name str | None None Service name shown in the Logfire UI.
send_to_logfire bool True Export spans to Logfire.
console Any None False to suppress console output, a logfire.ConsoleOptions, or None for environment defaults.
level ObservabilityLevel STANDARD Span detail level.
additional_span_processors list | None None Extra SpanProcessor instances.
**configure_kwargs Any Forwarded to logfire.configure().

Returns: setup_logfire returns None. Raises: ImportError if logfire is not installed.

Example:

logfire_tracing.py
from tenxgraph.core.graph import StateGraph
from tenxgraph.runtime.publisher import setup_logfire

graph = StateGraph()
# ... add nodes and edges

# Reads LOGFIRE_TOKEN from the environment
setup_logfire(graph, service_name="my-agent")
app = graph.compile()

LangsmithPublisher and setup_langsmith

LangsmithPublisher is an OtelPublisher that builds an OTLP HTTP exporter for LangSmith and installs it on a tracer provider. setup_langsmith does the same and also instruments a graph.

Requires: pip install "10xgraph[langsmith]"

Signature:

Python
class LangsmithPublisher(OtelPublisher):
    def __init__(
        self,
        *,
        api_key: str | None = None,
        project: str | None = None,
        endpoint: str = "https://api.smith.langchain.com/otel",
        level: ObservabilityLevel = ObservabilityLevel.STANDARD,
        tracer_provider: Any = None,
    )

def setup_langsmith(
    graph: StateGraph | None = None,
    *,
    api_key: str | None = None,
    project: str | None = None,
    endpoint: str = "https://api.smith.langchain.com/otel",
    level: ObservabilityLevel = ObservabilityLevel.STANDARD,
    tracer_provider: Any = None,
) -> None
Parameter Type Default Description
graph StateGraph | None None setup_langsmith only. None configures the exporter without attaching a publisher.
api_key str | None None LangSmith API key. Falls back to the LANGSMITH_API_KEY environment variable.
project str | None None Project name, sent as the Langsmith-Project header.
endpoint str https://api.smith.langchain.com/otel OTLP base URL. /v1/traces is appended. Override for regional deployments.
level ObservabilityLevel STANDARD Span detail level.
tracer_provider Any None Existing TracerProvider to attach to. A new global one is created when omitted.

Returns: setup_langsmith returns None. Raises: ImportError if the OTLP HTTP exporter or SDK is missing; ValueError if no API key is given and LANGSMITH_API_KEY is unset.

Example:

langsmith_tracing.py
from tenxgraph.core.graph import StateGraph
from tenxgraph.runtime.publisher import setup_langsmith

graph = StateGraph()
# ... add nodes and edges

# Reads LANGSMITH_API_KEY from the environment
setup_langsmith(graph, project="my-project")
app = graph.compile()

setup_observability

setup_observability enables Logfire, LangSmith or both from a config dict that mirrors the observability block of 10xgraph.json. When both are enabled they share one TracerProvider. If neither is enabled it does nothing.

Signature:

Python
def setup_observability(graph: StateGraph | None, config: dict[str, Any]) -> None
Parameter Type Description
graph StateGraph | None Graph to instrument. None configures providers only.
config dict[str, Any] Dict with level ("spans", "standard" or "full"; unknown values fall back to "standard"), logfire and langsmith blocks.
Config key Description
logfire.enabled Turn Logfire on. Default False.
logfire.service_name, logfire.send_to_logfire, logfire.console Passed to setup_logfire. send_to_logfire defaults to True.
langsmith.enabled Turn LangSmith on. Default False.
langsmith.project, langsmith.endpoint Project name and OTLP base URL.

Secrets (LOGFIRE_TOKEN, LANGSMITH_API_KEY) come from environment variables only, never from the config. It raises ImportError if a requested backend is not installed and ValueError if LangSmith is enabled without an API key.

Example:

observability.py
from tenxgraph.core.graph import StateGraph
from tenxgraph.runtime.publisher import setup_observability

graph = StateGraph()
# ... add nodes and edges

setup_observability(graph, {
    "level": "standard",
    "logfire": {"enabled": True, "service_name": "my-agent"},
    "langsmith": {"enabled": True, "project": "eval"},
})
app = graph.compile()

publish_event

publish_event emits a custom EventModel from inside a node or tool. It does not block: the event is handed to the background task manager, which calls the bound publisher. If no publisher is bound, it returns immediately. Publish failures are logged, not raised.

Signature:

Python
def publish_event(
    event: EventModel,
    publisher: BasePublisher | None = Inject[BasePublisher],
    task_manager: BackgroundTaskManager = Inject[BackgroundTaskManager],
) -> None
Parameter Type Default Description
event EventModel (required) The event to publish.
publisher BasePublisher | None Inject[BasePublisher] Injected. Leave it unset.
task_manager BackgroundTaskManager Inject[BackgroundTaskManager] Injected. Leave it unset.

Returns: None.

Example:

custom_event_tool.py
from tenxgraph.runtime.publisher import publish_event
from tenxgraph.runtime.publisher.events import ContentType, Event, EventModel, EventType
from tenxgraph.utils import tool


@tool
def shout(query: str) -> str:
    """Uppercase the query and emit a custom event."""
    result = query.upper()
    publish_event(
        EventModel(
            event=Event.TOOL_EXECUTION,
            event_type=EventType.RESULT,
            node_name="shout",
            content=result,
            content_type=[ContentType.TEXT],
            data={"query": query, "result_length": len(result)},
        )
    )
    return result

Writing a custom publisher

Subclass BasePublisher, call super().__init__(config), and implement publish, close and sync_close. Honor self._is_closed and keep close idempotent. This example posts events to a webhook with httpx (pip install httpx).

webhook_publisher.py
import asyncio
from typing import Any

import httpx

from tenxgraph.core.graph import StateGraph
from tenxgraph.runtime.publisher import BasePublisher
from tenxgraph.runtime.publisher.events import EventModel


class WebhookPublisher(BasePublisher):
    """Post each event as JSON to a webhook."""

    def __init__(self, webhook_url: str, config: dict[str, Any] | None = None):
        super().__init__(config or {})
        self.webhook_url = webhook_url
        self._client: httpx.AsyncClient | None = None

    async def publish(self, event: EventModel) -> None:
        if self._is_closed:
            raise RuntimeError("Cannot publish to closed WebhookPublisher")
        if self._client is None:
            self._client = httpx.AsyncClient()
        await self._client.post(self.webhook_url, json=event.model_dump(mode="json"))

    async def close(self) -> None:
        if self._is_closed:
            return
        if self._client is not None:
            await self._client.aclose()
            self._client = None
        self._is_closed = True

    def sync_close(self) -> None:
        asyncio.run(self.close())


publisher = WebhookPublisher("https://example.com/events")
graph = StateGraph(publisher=publisher)

Frequently asked questions

How do I attach a publisher to a graph?
Pass the publisher, or a list of publishers, to StateGraph(publisher=...). Tracing helpers such as setup_tracing and setup_logfire must run before compile().
What is the difference between bus publishers and tracing publishers?
Bus publishers (Console, Redis, Kafka, RabbitMQ) send each EventModel to a logger or message bus. Tracing publishers (OTEL, Logfire, LangSmith) turn events into spans.
Do I need to call publish_event myself?
No. The runtime publishes events automatically. Call publish_event only to emit a custom event from a node or tool.
Last updated for v0.10.0Edit this page on GitHubReport an issue